suemo 0.0.1 → 0.0.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 +37 -10
- package/package.json +1 -1
- package/src/cli/commands/believe.ts +22 -12
- package/src/cli/commands/consolidate.ts +18 -11
- package/src/cli/commands/doctor.ts +123 -0
- package/src/cli/commands/export-import.ts +58 -47
- package/src/cli/commands/goal.ts +52 -27
- package/src/cli/commands/health.ts +35 -17
- package/src/cli/commands/init.ts +155 -75
- package/src/cli/commands/observe.ts +25 -13
- package/src/cli/commands/query.ts +23 -7
- package/src/cli/commands/recall.ts +12 -6
- package/src/cli/commands/serve.ts +14 -5
- package/src/cli/commands/sync.ts +15 -6
- package/src/cli/commands/timeline.ts +30 -18
- package/src/cli/commands/wander.ts +27 -16
- package/src/cli/index.ts +3 -4
- package/src/cli/shared.ts +34 -0
- package/src/cognitive/consolidate.ts +48 -19
- package/src/cognitive/contradiction.ts +19 -7
- package/src/config.template.ts +36 -0
- package/src/config.ts +41 -12
- package/src/db/preflight.ts +32 -6
- package/src/db/schema.surql +11 -8
- package/src/db/schema.ts +6 -3
- package/src/embedding/index.ts +52 -0
- package/src/embedding/openai-compatible.ts +43 -0
- package/src/goal.ts +3 -1
- package/src/mcp/dispatch.ts +134 -0
- package/src/mcp/server.ts +25 -2
- package/src/mcp/stdio.ts +314 -0
- package/src/mcp/tools.ts +8 -89
- package/src/memory/read.ts +74 -19
- package/src/memory/write.ts +54 -18
- package/src/cli/commands/shared.ts +0 -20
package/src/memory/read.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
|
+
import type { SuemoConfig } from '../config.ts'
|
|
3
|
+
import { getEmbedding } from '../embedding/index.ts'
|
|
2
4
|
import { getLogger } from '../logger.ts'
|
|
3
5
|
import type { MemoryNode, QueryInput } from '../types.ts'
|
|
4
6
|
import { QueryInputSchema } from '../types.ts'
|
|
@@ -11,35 +13,70 @@ const ACTIVE_FILTER = '(valid_until = NONE OR valid_until > time::now())'
|
|
|
11
13
|
export async function query(
|
|
12
14
|
db: Surreal,
|
|
13
15
|
rawInput: QueryInput,
|
|
16
|
+
config: SuemoConfig,
|
|
14
17
|
): Promise<MemoryNode[]> {
|
|
15
18
|
const input = QueryInputSchema.parse(rawInput)
|
|
16
19
|
log.info('query()', { input: input.input, strategies: input.strategies, topK: input.topK })
|
|
17
20
|
|
|
18
|
-
const weights =
|
|
21
|
+
const weights = config.retrieval.weights
|
|
19
22
|
|
|
20
23
|
const topK = input.topK ?? 5
|
|
21
24
|
const strategies = input.strategies ?? ['vector', 'bm25', 'graph']
|
|
22
25
|
const candidateK = topK * 4 // over-fetch before reranking
|
|
26
|
+
const vectorProbeK = Math.max(1, Math.min(200, candidateK))
|
|
27
|
+
const vectorProbeEF = Math.max(40, vectorProbeK * 2)
|
|
23
28
|
const activeFilter = input.activeOnly ? ACTIVE_FILTER : 'true'
|
|
24
29
|
const scopeFilter = '($scope = NONE OR scope = $scope)'
|
|
25
30
|
const kindFilter = '($kinds = NONE OR kind INSIDE $kinds)'
|
|
31
|
+
const needsQueryEmbedding = strategies.includes('vector') || strategies.includes('bm25')
|
|
32
|
+
|| strategies.includes('graph')
|
|
33
|
+
|
|
34
|
+
let vectorExpr = 'fn::embed($queryText)'
|
|
35
|
+
let embeddingParam: number[] | undefined
|
|
36
|
+
if (needsQueryEmbedding) {
|
|
37
|
+
const embedding = await getEmbedding(input.input, config.embedding)
|
|
38
|
+
vectorExpr = embedding.clause === 'fn::embed($content)'
|
|
39
|
+
? 'fn::embed($queryText)'
|
|
40
|
+
: embedding.clause
|
|
41
|
+
embeddingParam = embedding.param
|
|
42
|
+
log.debug('query embedding resolved', {
|
|
43
|
+
provider: config.embedding.provider,
|
|
44
|
+
hasVectorParam: Boolean(embeddingParam),
|
|
45
|
+
dimension: embeddingParam?.length,
|
|
46
|
+
strategies,
|
|
47
|
+
})
|
|
48
|
+
}
|
|
26
49
|
|
|
27
50
|
const params = {
|
|
28
51
|
queryText: input.input,
|
|
29
52
|
scope: input.scope ?? null,
|
|
30
53
|
kinds: input.kind ?? null,
|
|
31
|
-
topK: input.topK,
|
|
32
54
|
candidateK,
|
|
55
|
+
...(embeddingParam ? { embedding: embeddingParam } : {}),
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
const promises: Promise<MemoryNode[]>[] = []
|
|
36
59
|
|
|
37
60
|
// ── Strategy A: RRF — vector + BM25 combined (one SurrealDB round trip) ──
|
|
38
61
|
if (strategies.includes('vector') || strategies.includes('bm25')) {
|
|
62
|
+
log.debug('query vector/bm25 strategy enabled', {
|
|
63
|
+
vectorProbeK,
|
|
64
|
+
vectorProbeEF,
|
|
65
|
+
weights: { vector: weights.vector, bm25: weights.bm25 },
|
|
66
|
+
})
|
|
39
67
|
promises.push(
|
|
40
68
|
db.query<[MemoryNode[]]>(
|
|
41
69
|
`
|
|
42
|
-
LET $vec =
|
|
70
|
+
LET $vec = ${vectorExpr};
|
|
71
|
+
LET $vecCand = (
|
|
72
|
+
SELECT id, embedding
|
|
73
|
+
FROM memory
|
|
74
|
+
WHERE ${activeFilter}
|
|
75
|
+
AND ${scopeFilter}
|
|
76
|
+
AND ${kindFilter}
|
|
77
|
+
AND embedding <|${vectorProbeK}, ${vectorProbeEF}|> $vec
|
|
78
|
+
);
|
|
79
|
+
|
|
43
80
|
SELECT *,
|
|
44
81
|
(
|
|
45
82
|
search::score(1) * ${weights.bm25} +
|
|
@@ -47,7 +84,7 @@ export async function query(
|
|
|
47
84
|
) AS _score
|
|
48
85
|
FROM memory
|
|
49
86
|
WHERE content @1@ $queryText
|
|
50
|
-
AND
|
|
87
|
+
AND id INSIDE $vecCand.id
|
|
51
88
|
AND ${activeFilter}
|
|
52
89
|
AND ${scopeFilter}
|
|
53
90
|
AND ${kindFilter}
|
|
@@ -62,16 +99,27 @@ export async function query(
|
|
|
62
99
|
// ── Strategy B: Graph spreading activation ───────────────────────────────
|
|
63
100
|
if (strategies.includes('graph')) {
|
|
64
101
|
// First get anchor IDs from a quick vector probe, then fan out via graph
|
|
102
|
+
log.debug('query graph strategy enabled', {
|
|
103
|
+
weights: { graph: weights.graph },
|
|
104
|
+
})
|
|
65
105
|
promises.push(
|
|
66
106
|
db.query<[MemoryNode[]]>(
|
|
67
107
|
`
|
|
68
|
-
LET $vec =
|
|
69
|
-
LET $
|
|
70
|
-
SELECT id
|
|
108
|
+
LET $vec = ${vectorExpr};
|
|
109
|
+
LET $anchor_cand = (
|
|
110
|
+
SELECT id, embedding
|
|
111
|
+
FROM memory
|
|
71
112
|
WHERE ${activeFilter}
|
|
72
|
-
|
|
113
|
+
AND embedding <|5, 20|> $vec
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
LET $anchor_rows = (
|
|
117
|
+
SELECT id, embedding, vector::similarity::cosine(embedding, $vec) AS _score
|
|
118
|
+
FROM $anchor_cand
|
|
119
|
+
ORDER BY _score DESC
|
|
73
120
|
LIMIT 5
|
|
74
|
-
)
|
|
121
|
+
);
|
|
122
|
+
LET $anchors = $anchor_rows.id;
|
|
75
123
|
SELECT *,
|
|
76
124
|
math::mean(<-relates_to[WHERE valid_until = NONE OR valid_until > time::now()].strength) AS _score
|
|
77
125
|
FROM memory
|
|
@@ -112,7 +160,7 @@ export async function query(
|
|
|
112
160
|
((b as Record<string, unknown>)['_score'] as number ?? 0)
|
|
113
161
|
- ((a as Record<string, unknown>)['_score'] as number ?? 0)
|
|
114
162
|
)
|
|
115
|
-
.slice(0,
|
|
163
|
+
.slice(0, topK)
|
|
116
164
|
.map(({ ...rest }) => {
|
|
117
165
|
delete (rest as Record<string, unknown>)['_score']
|
|
118
166
|
return rest as MemoryNode
|
|
@@ -130,13 +178,16 @@ export async function recall(
|
|
|
130
178
|
log.info('recall()', { nodeId })
|
|
131
179
|
|
|
132
180
|
const [nodeResult, neighborResult] = await Promise.all([
|
|
133
|
-
db.query<[MemoryNode[]]>(
|
|
181
|
+
db.query<[MemoryNode[]]>(
|
|
182
|
+
'SELECT * FROM <record<memory>>$id',
|
|
183
|
+
{ id: nodeId },
|
|
184
|
+
),
|
|
134
185
|
db.query<[MemoryNode[]]>(
|
|
135
186
|
`
|
|
136
187
|
SELECT * FROM memory
|
|
137
188
|
WHERE id INSIDE (
|
|
138
189
|
SELECT ->relates_to[WHERE valid_until = NONE OR valid_until > time::now()]->memory.id
|
|
139
|
-
FROM
|
|
190
|
+
FROM <record<memory>>$id
|
|
140
191
|
)
|
|
141
192
|
LIMIT 10
|
|
142
193
|
`,
|
|
@@ -150,7 +201,7 @@ export async function recall(
|
|
|
150
201
|
// FSRS tick — simplified next review interval
|
|
151
202
|
await db.query(
|
|
152
203
|
`
|
|
153
|
-
UPDATE
|
|
204
|
+
UPDATE <record<memory>>$id SET
|
|
154
205
|
fsrs_next_review = time::now() + 1d,
|
|
155
206
|
updated_at = time::now()
|
|
156
207
|
`,
|
|
@@ -171,9 +222,9 @@ export async function wander(
|
|
|
171
222
|
// If no anchor, pick a random high-salience active node
|
|
172
223
|
const anchorId = opts.anchor ?? await db.query<[{ id: string }[]]>(
|
|
173
224
|
`
|
|
174
|
-
SELECT id FROM memory
|
|
225
|
+
SELECT id, salience, (salience * rand()) AS _pick FROM memory
|
|
175
226
|
WHERE ${ACTIVE_FILTER} AND ($scope = NONE OR scope = $scope)
|
|
176
|
-
ORDER BY
|
|
227
|
+
ORDER BY _pick DESC
|
|
177
228
|
LIMIT 1
|
|
178
229
|
`,
|
|
179
230
|
{ scope: opts.scope ?? null },
|
|
@@ -203,20 +254,24 @@ export async function timeline(
|
|
|
203
254
|
opts: { scope?: string; from?: string; until?: string; limit?: number },
|
|
204
255
|
): Promise<MemoryNode[]> {
|
|
205
256
|
log.info('timeline()', opts)
|
|
257
|
+
const fromIso = opts.from ? new Date(opts.from).toISOString() : null
|
|
258
|
+
const untilIso = opts.until ? new Date(opts.until).toISOString() : null
|
|
259
|
+
const fromExpr = fromIso ? '<datetime>$from' : 'NONE'
|
|
260
|
+
const untilExpr = untilIso ? '<datetime>$until' : 'NONE'
|
|
206
261
|
return db.query<[MemoryNode[]]>(
|
|
207
262
|
`
|
|
208
263
|
SELECT * FROM memory
|
|
209
264
|
WHERE (valid_until = NONE OR valid_until > time::now())
|
|
210
265
|
AND ($scope = NONE OR scope = $scope)
|
|
211
|
-
AND ($
|
|
212
|
-
AND ($
|
|
266
|
+
AND (${fromExpr} = NONE OR created_at >= ${fromExpr})
|
|
267
|
+
AND (${untilExpr} = NONE OR created_at <= ${untilExpr})
|
|
213
268
|
ORDER BY created_at DESC
|
|
214
269
|
LIMIT $limit
|
|
215
270
|
`,
|
|
216
271
|
{
|
|
217
272
|
scope: opts.scope ?? null,
|
|
218
|
-
from:
|
|
219
|
-
until:
|
|
273
|
+
...(fromIso ? { from: fromIso } : {}),
|
|
274
|
+
...(untilIso ? { until: untilIso } : {}),
|
|
220
275
|
limit: opts.limit ?? 50,
|
|
221
276
|
},
|
|
222
277
|
).then((r) => r[0] ?? [])
|
package/src/memory/write.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Surreal } from 'surrealdb'
|
|
2
2
|
import { detectContradiction } from '../cognitive/contradiction.ts'
|
|
3
|
+
import type { SuemoConfig } from '../config.ts'
|
|
4
|
+
import { getEmbedding } from '../embedding/index.ts'
|
|
3
5
|
import { getLogger } from '../logger.ts'
|
|
4
6
|
import type { MemoryNode, ObserveInput } from '../types.ts'
|
|
5
7
|
import { ObserveInputSchema } from '../types.ts'
|
|
@@ -10,25 +12,57 @@ const log = getLogger(['suemo', 'memory', 'write'])
|
|
|
10
12
|
export async function observe(
|
|
11
13
|
db: Surreal,
|
|
12
14
|
rawInput: ObserveInput,
|
|
15
|
+
config: SuemoConfig,
|
|
13
16
|
): Promise<MemoryNode> {
|
|
14
17
|
const input = ObserveInputSchema.parse(rawInput)
|
|
15
18
|
log.info('observe()', { kind: input.kind, scope: input.scope, contentPreview: input.content.slice(0, 60) })
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
const { clause: embeddingClause, param: embeddingParam } = await getEmbedding(
|
|
21
|
+
input.content,
|
|
22
|
+
config.embedding,
|
|
23
|
+
)
|
|
24
|
+
log.debug('observe embedding resolved', {
|
|
25
|
+
provider: config.embedding.provider,
|
|
26
|
+
hasVectorParam: Boolean(embeddingParam),
|
|
27
|
+
dimension: embeddingParam?.length,
|
|
28
|
+
})
|
|
20
29
|
|
|
21
30
|
// 2. Dedup probe: ANN search for cosine similarity > 0.97
|
|
31
|
+
// For non-surreal providers, we pass the pre-computed vector as $qvec.
|
|
32
|
+
if (config.embedding.provider !== 'surreal' && !embeddingParam) {
|
|
33
|
+
throw new Error(`Missing embedding vector for provider: ${config.embedding.provider}`)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dedupVecClause = config.embedding.provider === 'surreal'
|
|
37
|
+
? 'fn::embed($content)'
|
|
38
|
+
: '$qvec'
|
|
39
|
+
const dedupParams = config.embedding.provider === 'surreal'
|
|
40
|
+
? { content: input.content }
|
|
41
|
+
: { qvec: embeddingParam }
|
|
42
|
+
log.debug('observe dedup probe', {
|
|
43
|
+
provider: config.embedding.provider,
|
|
44
|
+
queryMode: config.embedding.provider === 'surreal' ? 'fn::embed($content)' : '$qvec',
|
|
45
|
+
})
|
|
46
|
+
|
|
22
47
|
const dedupResult = await db.query<[{ id: string; score: number }[]]>(
|
|
23
48
|
`
|
|
24
|
-
LET $vec =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
LET $vec = ${dedupVecClause};
|
|
50
|
+
LET $cand = (
|
|
51
|
+
SELECT id, embedding
|
|
52
|
+
FROM memory
|
|
53
|
+
WHERE (valid_until = NONE OR valid_until > time::now())
|
|
54
|
+
AND embedding <|1, 20|> $vec
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
SELECT id, score
|
|
58
|
+
FROM (
|
|
59
|
+
SELECT id, vector::similarity::cosine(embedding, $vec) AS score
|
|
60
|
+
FROM $cand
|
|
61
|
+
)
|
|
62
|
+
ORDER BY score DESC
|
|
29
63
|
LIMIT 1
|
|
30
64
|
`,
|
|
31
|
-
|
|
65
|
+
dedupParams,
|
|
32
66
|
)
|
|
33
67
|
|
|
34
68
|
const topHit = dedupResult[0]?.[0]
|
|
@@ -39,8 +73,8 @@ export async function observe(
|
|
|
39
73
|
})
|
|
40
74
|
const updated = await db.query<[MemoryNode[]]>(
|
|
41
75
|
`
|
|
42
|
-
UPDATE
|
|
43
|
-
tags +=
|
|
76
|
+
UPDATE <record<memory>>$id SET
|
|
77
|
+
tags += <set<string>>$tags,
|
|
44
78
|
updated_at = time::now()
|
|
45
79
|
`,
|
|
46
80
|
{ id: topHit.id, tags: input.tags },
|
|
@@ -48,16 +82,16 @@ export async function observe(
|
|
|
48
82
|
return updated[0]![0]!
|
|
49
83
|
}
|
|
50
84
|
|
|
51
|
-
// 3. Create new node with
|
|
85
|
+
// 3. Create new node with embedding
|
|
52
86
|
const created = await db.query<[MemoryNode[]]>(
|
|
53
87
|
`
|
|
54
88
|
CREATE memory CONTENT {
|
|
55
89
|
kind: $kind,
|
|
56
90
|
content: $content,
|
|
57
91
|
summary: NONE,
|
|
58
|
-
tags:
|
|
92
|
+
tags: <set<string>>$tags,
|
|
59
93
|
scope: $scope,
|
|
60
|
-
embedding:
|
|
94
|
+
embedding: ${embeddingClause},
|
|
61
95
|
confidence: $confidence,
|
|
62
96
|
salience: 0.5,
|
|
63
97
|
source: $source,
|
|
@@ -72,9 +106,10 @@ export async function observe(
|
|
|
72
106
|
kind: input.kind,
|
|
73
107
|
content: input.content,
|
|
74
108
|
tags: input.tags,
|
|
75
|
-
scope: input.scope
|
|
109
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
76
110
|
confidence: input.confidence,
|
|
77
|
-
source: input.source
|
|
111
|
+
...(input.source !== undefined ? { source: input.source } : {}),
|
|
112
|
+
...(embeddingParam ? { embedding: embeddingParam } : {}),
|
|
78
113
|
},
|
|
79
114
|
)
|
|
80
115
|
|
|
@@ -89,10 +124,11 @@ export async function observe(
|
|
|
89
124
|
export async function believe(
|
|
90
125
|
db: Surreal,
|
|
91
126
|
input: Omit<ObserveInput, 'kind'>,
|
|
127
|
+
config: SuemoConfig,
|
|
92
128
|
): Promise<{ node: MemoryNode; contradicted?: MemoryNode }> {
|
|
93
129
|
log.info('believe()', { scope: input.scope })
|
|
94
130
|
|
|
95
|
-
const node = await observe(db, { ...input, kind: 'belief' })
|
|
131
|
+
const node = await observe(db, { ...input, kind: 'belief' }, config)
|
|
96
132
|
|
|
97
133
|
const contradicted = await detectContradiction(db, node)
|
|
98
134
|
if (contradicted) {
|
|
@@ -125,7 +161,7 @@ export async function invalidate(
|
|
|
125
161
|
log.info('invalidate()', { nodeId, reason })
|
|
126
162
|
await db.query(
|
|
127
163
|
`
|
|
128
|
-
UPDATE
|
|
164
|
+
UPDATE <record<memory>>$id SET
|
|
129
165
|
valid_until = time::now(),
|
|
130
166
|
updated_at = time::now()
|
|
131
167
|
`,
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { Crust } from '@crustjs/core'
|
|
2
|
-
|
|
3
|
-
// Root CLI — shared instance all commands are built off via .sub()
|
|
4
|
-
// The two flags here are inherited by every subcommand.
|
|
5
|
-
export const app = new Crust('suemo')
|
|
6
|
-
.meta({ description: 'Persistent semantic memory for AI agents — SurrealDB backend' })
|
|
7
|
-
.flags({
|
|
8
|
-
config: {
|
|
9
|
-
type: 'string',
|
|
10
|
-
short: 'c',
|
|
11
|
-
description: 'Path to suemo config file',
|
|
12
|
-
inherit: true,
|
|
13
|
-
},
|
|
14
|
-
debug: {
|
|
15
|
-
type: 'boolean',
|
|
16
|
-
short: 'd',
|
|
17
|
-
description: 'Enable verbose debug logging',
|
|
18
|
-
inherit: true,
|
|
19
|
-
},
|
|
20
|
-
})
|