@hanhnd/agent-kit 1.0.37 → 1.0.39
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/dist/cli.js +241 -112
- package/dist/mcp/memory.js +1 -1
- package/dist/server.js +244 -115
- package/dist/services/digest/digest-processor.test.js +1 -3
- package/dist/services/memory/constants.d.ts +3 -0
- package/dist/services/memory/constants.js +6 -0
- package/dist/services/memory/indexer.js +37 -54
- package/dist/services/memory/indexer.test.js +229 -231
- package/dist/services/memory/store.d.ts +4 -11
- package/dist/services/memory/store.js +206 -72
- package/dist/services/memory/store.test.js +359 -108
- package/dist/services/memory/types.d.ts +28 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { chunkMarkdown } from './chunker.js';
|
|
4
|
-
import { LOCK_RETRY_MS, LOCK_TIMEOUT_MS, RRF_K } from './constants.js';
|
|
4
|
+
import { DENSE_SCORE_FLOOR, FETCH_MULTIPLIER, LOCK_RETRY_MS, LOCK_TIMEOUT_MS, RECENCY_WEIGHT, RRF_K, } from './constants.js';
|
|
5
5
|
import { acquireLock, releaseLock } from '../../utils/files.js';
|
|
6
6
|
export function deriveSourceType(source) {
|
|
7
7
|
if (source.startsWith('compiled/provisional/conversation-digests/'))
|
|
@@ -32,7 +32,7 @@ export class MemoryIndexer {
|
|
|
32
32
|
const existingChunkIds = this.store.hashesBySource(source);
|
|
33
33
|
let text;
|
|
34
34
|
try {
|
|
35
|
-
text = fs.
|
|
35
|
+
text = await fs.promises.readFile(absolutePath, 'utf8');
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
38
|
console.warn(`[memory-indexer] Cannot read file ${absolutePath}:`, err);
|
|
@@ -40,7 +40,7 @@ export class MemoryIndexer {
|
|
|
40
40
|
}
|
|
41
41
|
let fileMtimeAt;
|
|
42
42
|
try {
|
|
43
|
-
fileMtimeAt = fs.
|
|
43
|
+
fileMtimeAt = (await fs.promises.stat(absolutePath)).mtimeMs;
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
|
46
46
|
console.warn(`[memory-indexer] Cannot stat file ${absolutePath}:`, err);
|
|
@@ -75,24 +75,34 @@ export class MemoryIndexer {
|
|
|
75
75
|
}
|
|
76
76
|
async indexDirectory(rootDir, opts = {}) {
|
|
77
77
|
const totals = { indexed: 0, deleted: 0, skipped: 0 };
|
|
78
|
-
|
|
78
|
+
let rootExists;
|
|
79
|
+
try {
|
|
80
|
+
await fs.promises.stat(rootDir);
|
|
81
|
+
rootExists = true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
rootExists = false;
|
|
85
|
+
}
|
|
86
|
+
if (!rootExists)
|
|
79
87
|
return totals;
|
|
80
88
|
const relativeBase = opts.relativeBase ?? this.config.wikiDir;
|
|
81
89
|
const excludeFiles = new Set(opts.excludeFiles ?? []);
|
|
82
90
|
const files = [];
|
|
83
|
-
const walk = (dirPath) => {
|
|
91
|
+
const walk = async (dirPath) => {
|
|
84
92
|
let entries;
|
|
85
93
|
try {
|
|
86
|
-
entries = fs.
|
|
94
|
+
entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
|
87
95
|
}
|
|
88
96
|
catch (err) {
|
|
89
97
|
console.warn(`[memory-indexer] Cannot scan directory ${dirPath}:`, err);
|
|
90
98
|
return;
|
|
91
99
|
}
|
|
100
|
+
// Sort for deterministic traversal order
|
|
101
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
92
102
|
for (const entry of entries) {
|
|
93
103
|
const entryPath = path.join(dirPath, entry.name);
|
|
94
104
|
if (entry.isDirectory()) {
|
|
95
|
-
walk(entryPath);
|
|
105
|
+
await walk(entryPath);
|
|
96
106
|
continue;
|
|
97
107
|
}
|
|
98
108
|
if (entry.isFile() && /\.md$/i.test(entry.name) && !excludeFiles.has(entry.name)) {
|
|
@@ -100,7 +110,7 @@ export class MemoryIndexer {
|
|
|
100
110
|
}
|
|
101
111
|
}
|
|
102
112
|
};
|
|
103
|
-
walk(rootDir);
|
|
113
|
+
await walk(rootDir);
|
|
104
114
|
const currentSources = new Set(files.map((file) => path.relative(relativeBase, file)));
|
|
105
115
|
// Remove stale sources (deleted files or pre-migration daily-file sources)
|
|
106
116
|
for (const stale of this.store.indexedSources()) {
|
|
@@ -118,65 +128,38 @@ export class MemoryIndexer {
|
|
|
118
128
|
return totals;
|
|
119
129
|
}
|
|
120
130
|
async search(query, topK) {
|
|
121
|
-
const fetchLimit = topK *
|
|
122
|
-
let
|
|
131
|
+
const fetchLimit = topK * FETCH_MULTIPLIER;
|
|
132
|
+
let embedding;
|
|
123
133
|
if (this.store.vecAvailable) {
|
|
124
134
|
try {
|
|
125
|
-
const
|
|
126
|
-
|
|
135
|
+
const embeddings = await this.embedder.embed([query]);
|
|
136
|
+
embedding = embeddings[0];
|
|
127
137
|
}
|
|
128
138
|
catch (err) {
|
|
129
139
|
console.warn('[memory-indexer] Dense search embedding failed:', err);
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
141
|
-
bm25Results.forEach((r, rank) => {
|
|
142
|
-
const entry = scoreMap.get(r.id) ?? { dense: 0, bm25: 0 };
|
|
143
|
-
entry.bm25 = 1 / (RRF_K + rank + 1);
|
|
144
|
-
scoreMap.set(r.id, entry);
|
|
142
|
+
const rows = this.store.searchHybrid({
|
|
143
|
+
query,
|
|
144
|
+
embedding,
|
|
145
|
+
topK,
|
|
146
|
+
fetchLimit,
|
|
147
|
+
denseScoreFloor: DENSE_SCORE_FLOOR,
|
|
148
|
+
recencyWeight: RECENCY_WEIGHT,
|
|
149
|
+
rrfK: RRF_K,
|
|
145
150
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const maxScore = numRetrievers / (RRF_K + 1);
|
|
149
|
-
const ranked = [...scoreMap.entries()]
|
|
150
|
-
.map(([id, scores]) => ({
|
|
151
|
-
id,
|
|
152
|
-
totalScore: scores.dense + scores.bm25,
|
|
153
|
-
hasDense: scores.dense > 0,
|
|
154
|
-
hasBm25: scores.bm25 > 0,
|
|
155
|
-
}))
|
|
156
|
-
.sort((a, b) => b.totalScore - a.totalScore);
|
|
157
|
-
const chunks = this.store.getChunksByIds(ranked.map((r) => r.id));
|
|
158
|
-
const chunkMap = new Map(chunks.map((c) => [c.id, c]));
|
|
151
|
+
if (rows.length === 0)
|
|
152
|
+
return [];
|
|
159
153
|
const results = [];
|
|
160
|
-
const
|
|
161
|
-
for (const r of ranked) {
|
|
162
|
-
if (results.length >= topK)
|
|
163
|
-
break;
|
|
164
|
-
const chunk = chunkMap.get(r.id);
|
|
165
|
-
if (!chunk)
|
|
166
|
-
continue;
|
|
167
|
-
if (seenSources.has(chunk.source))
|
|
168
|
-
continue;
|
|
169
|
-
seenSources.add(chunk.source);
|
|
170
|
-
const normalizedScore = maxScore > 0 ? r.totalScore / maxScore : 0;
|
|
171
|
-
const retriever = r.hasDense && r.hasBm25 ? 'both' : r.hasDense ? 'dense' : 'bm25';
|
|
154
|
+
for (const row of rows) {
|
|
172
155
|
let contentSource = 'file';
|
|
173
156
|
try {
|
|
174
|
-
chunk.content = fs.
|
|
157
|
+
row.chunk.content = await fs.promises.readFile(path.join(this.config.wikiDir, row.chunk.source), 'utf8');
|
|
175
158
|
}
|
|
176
159
|
catch {
|
|
177
160
|
contentSource = 'fallback';
|
|
178
161
|
}
|
|
179
|
-
results.push({ chunk, score:
|
|
162
|
+
results.push({ chunk: row.chunk, score: row.score, retriever: row.retriever, contentSource });
|
|
180
163
|
}
|
|
181
164
|
return results;
|
|
182
165
|
}
|
|
@@ -185,13 +168,13 @@ export class MemoryIndexer {
|
|
|
185
168
|
const rawDir = path.join(this.config.wikiDir, 'raw');
|
|
186
169
|
const savePath = path.join(rawDir, `conv_save_${datePart}.md`);
|
|
187
170
|
const lockPath = `${savePath}.lock`;
|
|
188
|
-
fs.
|
|
171
|
+
await fs.promises.mkdir(rawDir, { recursive: true });
|
|
189
172
|
const acquired = await acquireLock(lockPath, LOCK_TIMEOUT_MS, LOCK_RETRY_MS);
|
|
190
173
|
if (!acquired) {
|
|
191
174
|
console.warn('[memory-indexer] Could not acquire write lock, writing anyway (fail-open)');
|
|
192
175
|
}
|
|
193
176
|
try {
|
|
194
|
-
fs.
|
|
177
|
+
await fs.promises.appendFile(savePath, `\n${content}\n`, 'utf8');
|
|
195
178
|
}
|
|
196
179
|
finally {
|
|
197
180
|
if (acquired)
|