@swarmclawai/swarmclaw 0.6.7 → 0.7.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 +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -5,6 +5,7 @@ import { createHash } from 'crypto'
|
|
|
5
5
|
import { genId } from '@/lib/id'
|
|
6
6
|
import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
|
|
7
7
|
import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
|
|
8
|
+
import { applyMMR } from './mmr'
|
|
8
9
|
import { loadSettings } from './storage'
|
|
9
10
|
import {
|
|
10
11
|
normalizeLinkedMemoryIds,
|
|
@@ -23,8 +24,8 @@ const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
|
|
|
23
24
|
const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
|
|
24
25
|
export const MAX_FTS_QUERY_TERMS = 6
|
|
25
26
|
export const MAX_FTS_TERM_LENGTH = 48
|
|
26
|
-
const MAX_FTS_RESULT_ROWS =
|
|
27
|
-
const MAX_MERGED_RESULTS =
|
|
27
|
+
const MAX_FTS_RESULT_ROWS = 50
|
|
28
|
+
const MAX_MERGED_RESULTS = 80
|
|
28
29
|
|
|
29
30
|
export const MEMORY_FTS_STOP_WORDS = new Set([
|
|
30
31
|
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
|
|
@@ -33,6 +34,140 @@ export const MEMORY_FTS_STOP_WORDS = new Set([
|
|
|
33
34
|
'you', 'your',
|
|
34
35
|
])
|
|
35
36
|
|
|
37
|
+
export type MemoryScopeMode = 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project'
|
|
38
|
+
export type MemoryRerankMode = 'balanced' | 'semantic' | 'lexical'
|
|
39
|
+
|
|
40
|
+
export interface MemoryScopeFilter {
|
|
41
|
+
mode: MemoryScopeMode
|
|
42
|
+
agentId?: string | null
|
|
43
|
+
sessionId?: string | null
|
|
44
|
+
projectRoot?: string | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface MemorySearchOptions {
|
|
48
|
+
scope?: MemoryScopeFilter
|
|
49
|
+
rerankMode?: MemoryRerankMode
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeScopeIdentifier(value: unknown): string | null {
|
|
53
|
+
if (typeof value !== 'string') return null
|
|
54
|
+
const trimmed = value.trim()
|
|
55
|
+
return trimmed ? trimmed : null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function normalizePathForScope(value: unknown): string | null {
|
|
59
|
+
if (typeof value !== 'string') return null
|
|
60
|
+
const trimmed = value.trim()
|
|
61
|
+
if (!trimmed) return null
|
|
62
|
+
return path.normalize(trimmed).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function memorySearchText(entry: MemoryEntry): string {
|
|
66
|
+
const refs = Array.isArray(entry.references)
|
|
67
|
+
? entry.references.map((ref) => `${ref.type} ${ref.path || ''} ${ref.title || ''} ${ref.note || ''}`).join(' ')
|
|
68
|
+
: ''
|
|
69
|
+
return `${entry.title || ''} ${entry.content || ''} ${refs}`.toLowerCase()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tokenizeForRerank(input: string): string[] {
|
|
73
|
+
const raw = String(input || '').toLowerCase().match(/[a-z0-9][a-z0-9._:/-]*/g) || []
|
|
74
|
+
const out: string[] = []
|
|
75
|
+
const seen = new Set<string>()
|
|
76
|
+
for (const token of raw) {
|
|
77
|
+
if (token.length < 2) continue
|
|
78
|
+
if (MEMORY_FTS_STOP_WORDS.has(token)) continue
|
|
79
|
+
if (seen.has(token)) continue
|
|
80
|
+
seen.add(token)
|
|
81
|
+
out.push(token)
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function keywordOverlapScore(queryTokens: string[], entryText: string): number {
|
|
87
|
+
if (!queryTokens.length) return 0
|
|
88
|
+
const corpus = entryText.toLowerCase()
|
|
89
|
+
let matched = 0
|
|
90
|
+
for (const token of queryTokens) {
|
|
91
|
+
if (token.length < 3) continue
|
|
92
|
+
if (corpus.includes(token)) matched++
|
|
93
|
+
}
|
|
94
|
+
return matched / queryTokens.length
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function entryRootsForScope(entry: MemoryEntry): string[] {
|
|
98
|
+
const roots = new Set<string>()
|
|
99
|
+
const add = (raw: unknown) => {
|
|
100
|
+
const normalized = normalizePathForScope(raw)
|
|
101
|
+
if (normalized) roots.add(normalized)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
add((entry.metadata as Record<string, unknown> | undefined)?.projectRoot)
|
|
105
|
+
|
|
106
|
+
if (Array.isArray(entry.references)) {
|
|
107
|
+
for (const ref of entry.references) {
|
|
108
|
+
add(ref.projectRoot)
|
|
109
|
+
if (ref.type === 'project') add(ref.path)
|
|
110
|
+
if (ref.type === 'folder' || ref.type === 'file') add(ref.path)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(entry.filePaths)) {
|
|
115
|
+
for (const ref of entry.filePaths) {
|
|
116
|
+
add(ref.projectRoot)
|
|
117
|
+
add(ref.path)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [...roots]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function scopeAllowsAgentAccess(entry: MemoryEntry, agentId: string): boolean {
|
|
125
|
+
if (entry.agentId === agentId) return true
|
|
126
|
+
if (Array.isArray(entry.sharedWith) && entry.sharedWith.includes(agentId)) return true
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function normalizeMemoryScopeMode(raw: unknown): MemoryScopeMode {
|
|
131
|
+
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
|
132
|
+
if (value === 'shared') return 'global'
|
|
133
|
+
if (value === 'all' || value === 'global' || value === 'agent' || value === 'session' || value === 'project') return value
|
|
134
|
+
return 'auto'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function filterMemoriesByScope(entries: MemoryEntry[], scope?: MemoryScopeFilter): MemoryEntry[] {
|
|
138
|
+
if (!scope || scope.mode === 'all') return entries
|
|
139
|
+
const mode = normalizeMemoryScopeMode(scope.mode)
|
|
140
|
+
const agentId = normalizeScopeIdentifier(scope.agentId)
|
|
141
|
+
const sessionId = normalizeScopeIdentifier(scope.sessionId)
|
|
142
|
+
const projectRoot = normalizePathForScope(scope.projectRoot)
|
|
143
|
+
|
|
144
|
+
if (mode === 'global') {
|
|
145
|
+
return entries.filter((entry) => !entry.agentId)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (mode === 'agent') {
|
|
149
|
+
if (!agentId) return []
|
|
150
|
+
return entries.filter((entry) => scopeAllowsAgentAccess(entry, agentId))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (mode === 'session') {
|
|
154
|
+
if (!sessionId) return []
|
|
155
|
+
return entries.filter((entry) => entry.sessionId === sessionId)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (mode === 'project') {
|
|
159
|
+
if (!projectRoot) return []
|
|
160
|
+
return entries.filter((entry) => {
|
|
161
|
+
const roots = entryRootsForScope(entry)
|
|
162
|
+
return roots.some((root) => root === projectRoot || root.startsWith(`${projectRoot}/`))
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// auto
|
|
167
|
+
if (!agentId) return entries
|
|
168
|
+
return entries.filter((entry) => !entry.agentId || scopeAllowsAgentAccess(entry, agentId))
|
|
169
|
+
}
|
|
170
|
+
|
|
36
171
|
function computeContentHash(category: string, content: string): string {
|
|
37
172
|
const normalized = `${category}|${content.toLowerCase().trim()}`
|
|
38
173
|
return createHash('sha256').update(normalized).digest('hex').slice(0, 16)
|
|
@@ -611,8 +746,8 @@ function initDb() {
|
|
|
611
746
|
const getAllWithEmbeddings = db.prepare(
|
|
612
747
|
`SELECT * FROM memories WHERE embedding IS NOT NULL`
|
|
613
748
|
)
|
|
614
|
-
const
|
|
615
|
-
`SELECT * FROM memories WHERE embedding IS NOT NULL AND agentId =
|
|
749
|
+
const getAllWithEmbeddingsByAgentOrShared = db.prepare(
|
|
750
|
+
`SELECT * FROM memories WHERE embedding IS NOT NULL AND (agentId = ? OR sharedWith LIKE ?)`
|
|
616
751
|
)
|
|
617
752
|
|
|
618
753
|
return {
|
|
@@ -851,33 +986,54 @@ function initDb() {
|
|
|
851
986
|
return this.get(id)
|
|
852
987
|
},
|
|
853
988
|
|
|
854
|
-
search(query: string, agentId?: string): MemoryEntry[] {
|
|
989
|
+
search(query: string, agentId?: string, options: MemorySearchOptions = {}): MemoryEntry[] {
|
|
855
990
|
if (shouldSkipSearchQuery(query)) return []
|
|
856
991
|
const startedAt = Date.now()
|
|
992
|
+
const normalizedAgentId = normalizeScopeIdentifier(agentId)
|
|
993
|
+
const rerankMode: MemoryRerankMode = options.rerankMode === 'semantic' || options.rerankMode === 'lexical'
|
|
994
|
+
? options.rerankMode
|
|
995
|
+
: 'balanced'
|
|
996
|
+
const scopeMode = options.scope
|
|
997
|
+
? normalizeMemoryScopeMode(options.scope.mode)
|
|
998
|
+
: (normalizedAgentId ? 'agent' : 'all')
|
|
999
|
+
const scopeFilter: MemoryScopeFilter | undefined = scopeMode === 'all'
|
|
1000
|
+
? undefined
|
|
1001
|
+
: {
|
|
1002
|
+
mode: scopeMode,
|
|
1003
|
+
agentId: options.scope?.agentId ?? normalizedAgentId,
|
|
1004
|
+
sessionId: options.scope?.sessionId,
|
|
1005
|
+
projectRoot: options.scope?.projectRoot,
|
|
1006
|
+
}
|
|
1007
|
+
|
|
857
1008
|
// FTS keyword search (includes memories shared with this agent)
|
|
858
1009
|
const ftsQuery = buildFtsQuery(query)
|
|
1010
|
+
const fastAgentOnlyScope = scopeMode === 'agent' && !!normalizedAgentId
|
|
859
1011
|
const ftsResults: MemoryEntry[] = ftsQuery
|
|
860
|
-
? (
|
|
861
|
-
? stmts.searchByAgentOrShared.all(ftsQuery,
|
|
1012
|
+
? (fastAgentOnlyScope
|
|
1013
|
+
? stmts.searchByAgentOrShared.all(ftsQuery, normalizedAgentId, `%"${normalizedAgentId}"%`) as any[]
|
|
862
1014
|
: stmts.search.all(ftsQuery) as any[]
|
|
863
1015
|
).map(rowToEntry)
|
|
864
1016
|
: []
|
|
1017
|
+
const ftsHitIds = new Set<string>(ftsResults.map((entry) => entry.id))
|
|
865
1018
|
|
|
866
1019
|
// Attempt vector search (synchronous — uses cached embedding if available)
|
|
867
1020
|
const vectorSimilarityScores = new Map<string, number>()
|
|
1021
|
+
const rawEmbeddings = new Map<string, number[]>()
|
|
868
1022
|
let vectorResults: MemoryEntry[] = []
|
|
1023
|
+
let queryEmbeddingResult: number[] | undefined
|
|
869
1024
|
try {
|
|
870
1025
|
const queryEmbedding = getEmbeddingSync(query)
|
|
1026
|
+
queryEmbeddingResult = queryEmbedding || undefined
|
|
871
1027
|
if (queryEmbedding) {
|
|
872
|
-
const rows =
|
|
873
|
-
?
|
|
1028
|
+
const rows = fastAgentOnlyScope
|
|
1029
|
+
? getAllWithEmbeddingsByAgentOrShared.all(normalizedAgentId, `%"${normalizedAgentId}"%`) as any[]
|
|
874
1030
|
: getAllWithEmbeddings.all() as any[]
|
|
875
1031
|
|
|
876
1032
|
const scored = rows
|
|
877
1033
|
.map((row) => {
|
|
878
1034
|
const emb = deserializeEmbedding(row.embedding)
|
|
879
1035
|
const score = cosineSimilarity(queryEmbedding, emb)
|
|
880
|
-
return { row, score }
|
|
1036
|
+
return { row, score, emb }
|
|
881
1037
|
})
|
|
882
1038
|
.filter((s) => s.score > 0.3) // relevance threshold
|
|
883
1039
|
.sort((a, b) => b.score - a.score)
|
|
@@ -886,6 +1042,7 @@ function initDb() {
|
|
|
886
1042
|
vectorResults = scored.map((s) => {
|
|
887
1043
|
const entry = rowToEntry(s.row)
|
|
888
1044
|
vectorSimilarityScores.set(entry.id, s.score)
|
|
1045
|
+
rawEmbeddings.set(entry.id, s.emb)
|
|
889
1046
|
return entry
|
|
890
1047
|
})
|
|
891
1048
|
}
|
|
@@ -902,22 +1059,38 @@ function initDb() {
|
|
|
902
1059
|
merged.push(entry)
|
|
903
1060
|
}
|
|
904
1061
|
}
|
|
1062
|
+
const scopedMerged = scopeFilter ? filterMemoriesByScope(merged, scopeFilter) : merged
|
|
905
1063
|
|
|
906
|
-
//
|
|
1064
|
+
// Retrieval v2 rerank: hybrid relevance (semantic + lexical + FTS signal), then salience decay/boosting.
|
|
1065
|
+
const queryTokens = tokenizeForRerank(query)
|
|
907
1066
|
const now = Date.now()
|
|
908
1067
|
const HALF_LIFE_DAYS = 30
|
|
909
|
-
const salienceScored =
|
|
910
|
-
const
|
|
1068
|
+
const salienceScored = scopedMerged.map((entry) => {
|
|
1069
|
+
const semantic = vectorSimilarityScores.get(entry.id) ?? (ftsHitIds.has(entry.id) ? 0.42 : 0.18)
|
|
1070
|
+
const lexical = keywordOverlapScore(queryTokens, memorySearchText(entry))
|
|
1071
|
+
const ftsSignal = ftsHitIds.has(entry.id) ? 1 : 0
|
|
1072
|
+
const relevance = rerankMode === 'semantic'
|
|
1073
|
+
? (semantic * 0.78 + ftsSignal * 0.22)
|
|
1074
|
+
: rerankMode === 'lexical'
|
|
1075
|
+
? (lexical * 0.78 + ftsSignal * 0.22)
|
|
1076
|
+
: (semantic * 0.50 + lexical * 0.35 + ftsSignal * 0.15)
|
|
911
1077
|
const daysSinceAccess = (now - (entry.lastAccessedAt || entry.updatedAt)) / 86_400_000
|
|
912
1078
|
const recencyDecay = Math.exp(-0.693 * daysSinceAccess / HALF_LIFE_DAYS)
|
|
913
1079
|
const reinforcement = Math.log((entry.reinforcementCount || 0) + 1) + 1
|
|
914
1080
|
const pinnedBoost = entry.pinned ? 1.5 : 1.0
|
|
915
|
-
const salience =
|
|
916
|
-
return { entry, salience }
|
|
1081
|
+
const salience = Math.max(0.0001, relevance) * recencyDecay * reinforcement * pinnedBoost
|
|
1082
|
+
return { entry, salience, embedding: rawEmbeddings.get(entry.id) }
|
|
917
1083
|
})
|
|
918
|
-
salienceScored.sort((a, b) => b.salience - a.salience)
|
|
919
1084
|
|
|
920
|
-
|
|
1085
|
+
// Apply MMR when semantic reranking is enabled and query embeddings are available.
|
|
1086
|
+
let out: MemoryEntry[] = []
|
|
1087
|
+
const useMmr = !!queryEmbeddingResult && rerankMode !== 'lexical'
|
|
1088
|
+
if (useMmr && queryEmbeddingResult) {
|
|
1089
|
+
out = applyMMR(queryEmbeddingResult, salienceScored, MAX_MERGED_RESULTS, 0.6)
|
|
1090
|
+
} else {
|
|
1091
|
+
salienceScored.sort((a, b) => b.salience - a.salience)
|
|
1092
|
+
out = salienceScored.slice(0, MAX_MERGED_RESULTS).map((s) => s.entry)
|
|
1093
|
+
}
|
|
921
1094
|
|
|
922
1095
|
// Bump access counts for returned results (non-blocking)
|
|
923
1096
|
if (out.length) {
|
|
@@ -933,7 +1106,7 @@ function initDb() {
|
|
|
933
1106
|
const elapsed = Date.now() - startedAt
|
|
934
1107
|
if (elapsed > 1200) {
|
|
935
1108
|
console.warn(
|
|
936
|
-
`[memory-db] Slow search ${elapsed}ms (
|
|
1109
|
+
`[memory-db] Slow search ${elapsed}ms (scope=${scopeMode}, rerank=${rerankMode}, rawLen=${String(query || '').length}, fts="${ftsQuery.slice(0, 180)}")`,
|
|
937
1110
|
)
|
|
938
1111
|
}
|
|
939
1112
|
return out
|
|
@@ -946,8 +1119,9 @@ function initDb() {
|
|
|
946
1119
|
maxDepth?: number,
|
|
947
1120
|
maxResults?: number,
|
|
948
1121
|
maxLinkedExpansion?: number,
|
|
1122
|
+
options: MemorySearchOptions = {},
|
|
949
1123
|
): { entries: MemoryEntry[]; truncated: boolean; expandedLinkedCount: number; limits: MemoryLookupLimits } {
|
|
950
|
-
const baseResults = this.search(query, agentId)
|
|
1124
|
+
const baseResults = this.search(query, agentId, options)
|
|
951
1125
|
const defaults = getMemoryLookupLimits()
|
|
952
1126
|
const limits = resolveLookupRequest(defaults, {
|
|
953
1127
|
depth: maxDepth ?? defaults.maxDepth,
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import type { MemoryEntry } from '@/types'
|
|
4
|
+
import { filterMemoriesByScope, normalizeMemoryScopeMode } from './memory-db.ts'
|
|
5
|
+
|
|
6
|
+
function makeEntry(id: string, patch: Partial<MemoryEntry> = {}): MemoryEntry {
|
|
7
|
+
return {
|
|
8
|
+
id,
|
|
9
|
+
agentId: null,
|
|
10
|
+
sessionId: null,
|
|
11
|
+
category: 'note',
|
|
12
|
+
title: `Entry ${id}`,
|
|
13
|
+
content: `content ${id}`,
|
|
14
|
+
createdAt: 1,
|
|
15
|
+
updatedAt: 1,
|
|
16
|
+
...patch,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test('normalizeMemoryScopeMode maps shared alias to global', () => {
|
|
21
|
+
assert.equal(normalizeMemoryScopeMode('shared'), 'global')
|
|
22
|
+
assert.equal(normalizeMemoryScopeMode('project'), 'project')
|
|
23
|
+
assert.equal(normalizeMemoryScopeMode('unknown'), 'auto')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('filterMemoriesByScope auto includes global + own + shared-with-agent', () => {
|
|
27
|
+
const rows: MemoryEntry[] = [
|
|
28
|
+
makeEntry('global', { agentId: null }),
|
|
29
|
+
makeEntry('mine', { agentId: 'agent-a' }),
|
|
30
|
+
makeEntry('shared', { agentId: 'agent-b', sharedWith: ['agent-a'] }),
|
|
31
|
+
makeEntry('other', { agentId: 'agent-b' }),
|
|
32
|
+
]
|
|
33
|
+
const filtered = filterMemoriesByScope(rows, { mode: 'auto', agentId: 'agent-a' })
|
|
34
|
+
assert.deepEqual(filtered.map((r) => r.id), ['global', 'mine', 'shared'])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('filterMemoriesByScope supports project and session scopes', () => {
|
|
38
|
+
const rows: MemoryEntry[] = [
|
|
39
|
+
makeEntry('proj-hit', {
|
|
40
|
+
sessionId: 's-1',
|
|
41
|
+
references: [{ type: 'project', path: '/repo/swarm', projectRoot: '/repo/swarm', timestamp: 1 }],
|
|
42
|
+
}),
|
|
43
|
+
makeEntry('proj-miss', {
|
|
44
|
+
sessionId: 's-1',
|
|
45
|
+
references: [{ type: 'project', path: '/repo/other', projectRoot: '/repo/other', timestamp: 1 }],
|
|
46
|
+
}),
|
|
47
|
+
makeEntry('session-hit', { sessionId: 's-2' }),
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const projectFiltered = filterMemoriesByScope(rows, { mode: 'project', projectRoot: '/repo/swarm' })
|
|
51
|
+
assert.deepEqual(projectFiltered.map((r) => r.id), ['proj-hit'])
|
|
52
|
+
|
|
53
|
+
const sessionFiltered = filterMemoriesByScope(rows, { mode: 'session', sessionId: 's-2' })
|
|
54
|
+
assert.deepEqual(sessionFiltered.map((r) => r.id), ['session-hit'])
|
|
55
|
+
})
|
|
56
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { cosineSimilarity } from './embeddings'
|
|
2
|
+
import type { MemoryEntry } from '@/types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Applies Maximal Marginal Relevance (MMR) to diversify search results.
|
|
6
|
+
* It balances relevance to the query (salience/similarity) against novelty
|
|
7
|
+
* compared to already-selected documents.
|
|
8
|
+
*/
|
|
9
|
+
export function applyMMR(
|
|
10
|
+
queryEmbedding: number[],
|
|
11
|
+
candidates: Array<{ entry: MemoryEntry; salience: number; embedding?: number[] }>,
|
|
12
|
+
limit: number,
|
|
13
|
+
lambda: number = 0.5
|
|
14
|
+
): MemoryEntry[] {
|
|
15
|
+
if (candidates.length === 0) return []
|
|
16
|
+
|
|
17
|
+
// Normalize salience to [0, 1] range
|
|
18
|
+
const maxSalience = Math.max(...candidates.map(c => c.salience))
|
|
19
|
+
const minSalience = Math.min(...candidates.map(c => c.salience))
|
|
20
|
+
const salienceRange = maxSalience - minSalience || 1
|
|
21
|
+
|
|
22
|
+
const candidatesWithNormalizedSalience = candidates.map(c => ({
|
|
23
|
+
...c,
|
|
24
|
+
normSalience: (c.salience - minSalience) / salienceRange
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const selected: typeof candidatesWithNormalizedSalience = []
|
|
28
|
+
const remaining = [...candidatesWithNormalizedSalience]
|
|
29
|
+
|
|
30
|
+
// Debug: uncomment for troubleshooting
|
|
31
|
+
// console.log(`[mmr] Starting MMR for ${remaining.length} candidates, limit=${limit}, lambda=${lambda}`)
|
|
32
|
+
|
|
33
|
+
while (selected.length < limit && remaining.length > 0) {
|
|
34
|
+
let bestMmrScore = -Infinity
|
|
35
|
+
let bestIndex = -1
|
|
36
|
+
|
|
37
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
38
|
+
const candidate = remaining[i]
|
|
39
|
+
|
|
40
|
+
let maxSimToSelected = 0
|
|
41
|
+
if (selected.length > 0 && candidate.embedding) {
|
|
42
|
+
for (const sel of selected) {
|
|
43
|
+
if (sel.embedding) {
|
|
44
|
+
const sim = cosineSimilarity(candidate.embedding, sel.embedding)
|
|
45
|
+
if (sim > maxSimToSelected) maxSimToSelected = sim
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MMR Score = Lambda * Relevance - (1 - Lambda) * Diversity (max similarity to selected)
|
|
51
|
+
const mmrScore = (lambda * candidate.normSalience) - ((1 - lambda) * maxSimToSelected)
|
|
52
|
+
|
|
53
|
+
// DEBUG LOG
|
|
54
|
+
// console.log(` Candidate ${candidate.entry.id}: rel=${candidate.normSalience.toFixed(3)}, div_penalty=${maxSimToSelected.toFixed(3)}, mmr=${mmrScore.toFixed(3)}`)
|
|
55
|
+
|
|
56
|
+
if (mmrScore > bestMmrScore) {
|
|
57
|
+
bestMmrScore = mmrScore
|
|
58
|
+
bestIndex = i
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (bestIndex !== -1) {
|
|
63
|
+
const picked = remaining[bestIndex]
|
|
64
|
+
// console.log(`[mmr] Picked ${picked.entry.id} with score ${bestMmrScore.toFixed(3)}`)
|
|
65
|
+
selected.push(picked)
|
|
66
|
+
remaining.splice(bestIndex, 1)
|
|
67
|
+
} else {
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return selected.map(s => s.entry)
|
|
73
|
+
}
|
|
@@ -12,6 +12,7 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
|
|
|
12
12
|
import { notify } from './ws-hub'
|
|
13
13
|
import { pushMainLoopEventToMainSessions } from './main-agent-loop'
|
|
14
14
|
import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
|
|
15
|
+
import { getPluginManager } from './plugins'
|
|
15
16
|
import { genId } from '@/lib/id'
|
|
16
17
|
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
17
18
|
import type { Agent, TaskComment, MessageToolEvent } from '@/types'
|
|
@@ -176,6 +177,7 @@ export async function executeLangGraphOrchestrator(
|
|
|
176
177
|
return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
|
|
177
178
|
}
|
|
178
179
|
console.log(`[orchestrator-lg] Delegating to ${agent.name}: ${agentTask.slice(0, 80)}`)
|
|
180
|
+
getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
|
|
179
181
|
const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
|
|
180
182
|
saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
|
|
181
183
|
name: 'delegate_to_agent',
|
|
@@ -602,7 +604,10 @@ export async function executeLangGraphOrchestrator(
|
|
|
602
604
|
|
|
603
605
|
// Extract summary from mark_complete if present
|
|
604
606
|
const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
|
|
605
|
-
|
|
607
|
+
const summary = completeMatch ? completeMatch[1].trim() : finalResult
|
|
608
|
+
|
|
609
|
+
// Append meta so the main-loop knows we are done
|
|
610
|
+
return `${summary}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(summary.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
|
|
606
611
|
}
|
|
607
612
|
|
|
608
613
|
/**
|
|
@@ -623,6 +628,7 @@ export async function resumeLangGraphOrchestrator(
|
|
|
623
628
|
async ({ agentName, task: agentTask }) => {
|
|
624
629
|
const agent = agents.find((a) => a.name.toLowerCase() === agentName.toLowerCase())
|
|
625
630
|
if (!agent) return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
|
|
631
|
+
getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
|
|
626
632
|
const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
|
|
627
633
|
saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
|
|
628
634
|
name: 'delegate_to_agent',
|
|
@@ -77,13 +77,14 @@ export async function executeOrchestrator(
|
|
|
77
77
|
|
|
78
78
|
// claude-cli fallback (no structured tool calling)
|
|
79
79
|
console.warn(`[orchestrator] Using legacy regex-based engine for ${orchestrator.name} (claude-cli)`)
|
|
80
|
-
return executeOrchestratorLegacy(orchestrator, task, sessionId)
|
|
80
|
+
return executeOrchestratorLegacy(orchestrator, task, sessionId, taskId)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
async function executeOrchestratorLegacy(
|
|
84
84
|
orchestrator: Agent,
|
|
85
85
|
task: string,
|
|
86
86
|
sessionId: string,
|
|
87
|
+
taskId?: string,
|
|
87
88
|
): Promise<string> {
|
|
88
89
|
const allAgents = loadAgents()
|
|
89
90
|
const sessions = loadSessions()
|
|
@@ -240,7 +241,7 @@ async function executeOrchestratorLegacy(
|
|
|
240
241
|
|
|
241
242
|
if (cmd.done) {
|
|
242
243
|
result = cmd.summary || fullText
|
|
243
|
-
return result
|
|
244
|
+
return `${result}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(result.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
|
|
@@ -255,7 +256,7 @@ async function executeOrchestratorLegacy(
|
|
|
255
256
|
result = `Loop stopped after reaching max turns (${maxTurns}).`
|
|
256
257
|
}
|
|
257
258
|
|
|
258
|
-
return result
|
|
259
|
+
return `${result}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(result.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
async function executeSubTask(
|