@modusensus/dsh-mneme 0.1.5 → 0.2.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/LICENSE +21 -21
- package/README.md +239 -188
- package/cordis.patch.yml +15 -15
- package/lib/api.js +277 -245
- package/lib/client.js +461 -461
- package/lib/commands.js +64 -64
- package/lib/config.js +49 -15
- package/lib/dream/clustering.js +118 -0
- package/lib/dream/decisions.js +128 -121
- package/lib/dream.js +428 -209
- package/lib/embedding.js +97 -97
- package/lib/index.js +176 -120
- package/lib/inject.js +47 -47
- package/lib/local-embedder.js +265 -0
- package/lib/mirror.js +131 -131
- package/lib/reranker.js +203 -0
- package/lib/service.js +268 -174
- package/lib/settings.js +142 -142
- package/lib/store.js +409 -310
- package/lib/summarize.js +171 -171
- package/lib/tools.js +230 -221
- package/lib/vector-index.js +106 -0
- package/package.json +8 -4
- package/src/api.js +277 -245
- package/src/commands.js +64 -64
- package/src/config.js +49 -15
- package/src/dream/clustering.js +118 -0
- package/src/dream/decisions.js +128 -121
- package/src/dream.js +428 -209
- package/src/embedding.js +97 -97
- package/src/index.js +176 -120
- package/src/inject.js +47 -47
- package/src/local-embedder.js +265 -0
- package/src/mirror.js +131 -131
- package/src/reranker.js +203 -0
- package/src/service.js +268 -174
- package/src/settings.js +142 -142
- package/src/store.js +409 -310
- package/src/summarize.js +171 -171
- package/src/tools.js +230 -221
- package/src/vector-index.js +106 -0
package/src/embedding.js
CHANGED
|
@@ -1,97 +1,97 @@
|
|
|
1
|
-
// OpenAI-compatible embedding client for vector search. DSH's LLM service is
|
|
2
|
-
// chat-only, so dsh-mneme calls an external `/embeddings` endpoint itself.
|
|
3
|
-
// Works with OpenAI, SiliconFlow, Zhipu, local Ollama (via OpenAI-compatible
|
|
4
|
-
// proxy) and any provider exposing the standard embeddings API.
|
|
5
|
-
const DEFAULT_TIMEOUT_MS = 15000;
|
|
6
|
-
|
|
7
|
-
/** Normalize a configured baseUrl into the full embeddings endpoint URL. */
|
|
8
|
-
function embeddingsUrl(baseUrl) {
|
|
9
|
-
const base = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
10
|
-
if (!base) return "";
|
|
11
|
-
// Accept both "https://host/v1" and a full path ending in /embeddings.
|
|
12
|
-
if (/\/embeddings$/i.test(base)) return base;
|
|
13
|
-
return `${base}/embeddings`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Call the embeddings API for one text. Resolves to a Float64 array, or null
|
|
18
|
-
* when the provider is not configured, the call fails, or the response is
|
|
19
|
-
* unusable. Never throws: failures degrade to keyword search.
|
|
20
|
-
*/
|
|
21
|
-
export async function embedText({ baseUrl, apiKey, model }, text) {
|
|
22
|
-
const url = embeddingsUrl(baseUrl);
|
|
23
|
-
if (!url || !apiKey || !model || !text) return null;
|
|
24
|
-
let res;
|
|
25
|
-
try {
|
|
26
|
-
res = await fetch(url, {
|
|
27
|
-
method: "POST",
|
|
28
|
-
headers: {
|
|
29
|
-
"Content-Type": "application/json",
|
|
30
|
-
"Authorization": `Bearer ${apiKey}`
|
|
31
|
-
},
|
|
32
|
-
body: JSON.stringify({ model, input: String(text).slice(0, 8000) }),
|
|
33
|
-
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
34
|
-
});
|
|
35
|
-
} catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
if (!res.ok) return null;
|
|
39
|
-
let body;
|
|
40
|
-
try {
|
|
41
|
-
body = await res.json();
|
|
42
|
-
} catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
const vec = body?.data?.[0]?.embedding;
|
|
46
|
-
return Array.isArray(vec) && vec.length ? Array.from(vec) : null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Embedder bound to the current settings + store: on each write it re-embeds
|
|
51
|
-
* the row's title+content and stores the vector. Failures are swallowed so a
|
|
52
|
-
* flaky embedding endpoint never breaks memory writes.
|
|
53
|
-
*/
|
|
54
|
-
export function createEmbedder({ store, settings, logger }) {
|
|
55
|
-
async function embedFor(id, title, content) {
|
|
56
|
-
const cfg = settings.getVectorConfig();
|
|
57
|
-
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return;
|
|
58
|
-
const text = [title, content].filter(Boolean).join("\n");
|
|
59
|
-
const vector = await embedText(cfg, text);
|
|
60
|
-
if (vector) {
|
|
61
|
-
store.setEmbedding(id, vector);
|
|
62
|
-
logger?.info?.(`[dsh-mneme] embedded memory ${id} (dim=${vector.length})`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
/** Fire-and-forget re-embed of a memory after any write. */
|
|
68
|
-
schedule(memory) {
|
|
69
|
-
if (!memory?.id) return;
|
|
70
|
-
embedFor(memory.id, memory.title, memory.content).catch(() => {});
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
/** Embed one text and return its vector (null on failure/disabled). */
|
|
74
|
-
async embed(query) {
|
|
75
|
-
const cfg = settings.getVectorConfig();
|
|
76
|
-
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return null;
|
|
77
|
-
return embedText(cfg, query);
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
/** Batch re-index rows still missing an embedding. */
|
|
81
|
-
async reindexMissing(limit = 50) {
|
|
82
|
-
const cfg = settings.getVectorConfig();
|
|
83
|
-
if (!cfg?.enabled) return { indexed: 0, skipped: 0 };
|
|
84
|
-
const rows = store.needsEmbedding(limit);
|
|
85
|
-
let indexed = 0;
|
|
86
|
-
for (const row of rows) {
|
|
87
|
-
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
88
|
-
const vector = await embedText(cfg, text);
|
|
89
|
-
if (vector) {
|
|
90
|
-
store.setEmbedding(row.id, vector);
|
|
91
|
-
indexed++;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return { indexed, skipped: rows.length - indexed };
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
}
|
|
1
|
+
// OpenAI-compatible embedding client for vector search. DSH's LLM service is
|
|
2
|
+
// chat-only, so dsh-mneme calls an external `/embeddings` endpoint itself.
|
|
3
|
+
// Works with OpenAI, SiliconFlow, Zhipu, local Ollama (via OpenAI-compatible
|
|
4
|
+
// proxy) and any provider exposing the standard embeddings API.
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
6
|
+
|
|
7
|
+
/** Normalize a configured baseUrl into the full embeddings endpoint URL. */
|
|
8
|
+
function embeddingsUrl(baseUrl) {
|
|
9
|
+
const base = String(baseUrl ?? "").trim().replace(/\/+$/, "");
|
|
10
|
+
if (!base) return "";
|
|
11
|
+
// Accept both "https://host/v1" and a full path ending in /embeddings.
|
|
12
|
+
if (/\/embeddings$/i.test(base)) return base;
|
|
13
|
+
return `${base}/embeddings`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Call the embeddings API for one text. Resolves to a Float64 array, or null
|
|
18
|
+
* when the provider is not configured, the call fails, or the response is
|
|
19
|
+
* unusable. Never throws: failures degrade to keyword search.
|
|
20
|
+
*/
|
|
21
|
+
export async function embedText({ baseUrl, apiKey, model }, text) {
|
|
22
|
+
const url = embeddingsUrl(baseUrl);
|
|
23
|
+
if (!url || !apiKey || !model || !text) return null;
|
|
24
|
+
let res;
|
|
25
|
+
try {
|
|
26
|
+
res = await fetch(url, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
"Authorization": `Bearer ${apiKey}`
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify({ model, input: String(text).slice(0, 8000) }),
|
|
33
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (!res.ok) return null;
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = await res.json();
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const vec = body?.data?.[0]?.embedding;
|
|
46
|
+
return Array.isArray(vec) && vec.length ? Array.from(vec) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Embedder bound to the current settings + store: on each write it re-embeds
|
|
51
|
+
* the row's title+content and stores the vector. Failures are swallowed so a
|
|
52
|
+
* flaky embedding endpoint never breaks memory writes.
|
|
53
|
+
*/
|
|
54
|
+
export function createEmbedder({ store, settings, logger }) {
|
|
55
|
+
async function embedFor(id, title, content) {
|
|
56
|
+
const cfg = settings.getVectorConfig();
|
|
57
|
+
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return;
|
|
58
|
+
const text = [title, content].filter(Boolean).join("\n");
|
|
59
|
+
const vector = await embedText(cfg, text);
|
|
60
|
+
if (vector) {
|
|
61
|
+
store.setEmbedding(id, vector);
|
|
62
|
+
logger?.info?.(`[dsh-mneme] embedded memory ${id} (dim=${vector.length})`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
/** Fire-and-forget re-embed of a memory after any write. */
|
|
68
|
+
schedule(memory) {
|
|
69
|
+
if (!memory?.id) return;
|
|
70
|
+
embedFor(memory.id, memory.title, memory.content).catch(() => {});
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
/** Embed one text and return its vector (null on failure/disabled). */
|
|
74
|
+
async embed(query) {
|
|
75
|
+
const cfg = settings.getVectorConfig();
|
|
76
|
+
if (!cfg?.enabled || !cfg.baseUrl || !cfg.apiKey || !cfg.model) return null;
|
|
77
|
+
return embedText(cfg, query);
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/** Batch re-index rows still missing an embedding. */
|
|
81
|
+
async reindexMissing(limit = 50) {
|
|
82
|
+
const cfg = settings.getVectorConfig();
|
|
83
|
+
if (!cfg?.enabled) return { indexed: 0, skipped: 0 };
|
|
84
|
+
const rows = store.needsEmbedding(limit);
|
|
85
|
+
let indexed = 0;
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
88
|
+
const vector = await embedText(cfg, text);
|
|
89
|
+
if (vector) {
|
|
90
|
+
store.setEmbedding(row.id, vector);
|
|
91
|
+
indexed++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { indexed, skipped: rows.length - indexed };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,120 +1,176 @@
|
|
|
1
|
-
import { createStore } from "./store.js";
|
|
2
|
-
import { createMirror, TYPE_FILE } from "./mirror.js";
|
|
3
|
-
import { createService } from "./service.js";
|
|
4
|
-
import { createTools } from "./tools.js";
|
|
5
|
-
import { createInjector } from "./inject.js";
|
|
6
|
-
import { createSummarizer } from "./summarize.js";
|
|
7
|
-
import { createDreamScheduler } from "./dream.js";
|
|
8
|
-
import { createApi } from "./api.js";
|
|
9
|
-
import { createSettings } from "./settings.js";
|
|
10
|
-
import { createCommandManager } from "./commands.js";
|
|
11
|
-
import { createEmbedder } from "./embedding.js";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
1
|
+
import { createStore } from "./store.js";
|
|
2
|
+
import { createMirror, TYPE_FILE } from "./mirror.js";
|
|
3
|
+
import { createService } from "./service.js";
|
|
4
|
+
import { createTools } from "./tools.js";
|
|
5
|
+
import { createInjector } from "./inject.js";
|
|
6
|
+
import { createSummarizer } from "./summarize.js";
|
|
7
|
+
import { createDreamScheduler } from "./dream.js";
|
|
8
|
+
import { createApi } from "./api.js";
|
|
9
|
+
import { createSettings } from "./settings.js";
|
|
10
|
+
import { createCommandManager } from "./commands.js";
|
|
11
|
+
import { createEmbedder } from "./embedding.js";
|
|
12
|
+
import { createEmbedderByProvider } from "./local-embedder.js";
|
|
13
|
+
import { LocalReranker } from "./reranker.js";
|
|
14
|
+
import { createVectorIndex } from "./vector-index.js";
|
|
15
|
+
import { Config } from "./config.js";
|
|
16
|
+
import { mkdirSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { homedir } from "node:os";
|
|
19
|
+
|
|
20
|
+
export const name = "dsh-mneme";
|
|
21
|
+
export const inject = ["tools", "systemPrompt", "webServer", "llm", "agentDefaultModel", "commands"];
|
|
22
|
+
export { Config };
|
|
23
|
+
|
|
24
|
+
// Arrow (not function declaration): cordis 4 treats any apply with a
|
|
25
|
+
// prototype as a class constructor (`new apply(...)`) and discards its return
|
|
26
|
+
// value, so a `function apply` disposer would never run on unload. An arrow
|
|
27
|
+
// has no prototype, is called normally, and its returned disposer is collected
|
|
28
|
+
// and run by the fiber on unload.
|
|
29
|
+
export const apply = (ctx, config) => {
|
|
30
|
+
const cfg = Config(config);
|
|
31
|
+
|
|
32
|
+
// Resolve memoryDir: expand leading "~"
|
|
33
|
+
const memoryDir = cfg.memoryDir.startsWith("~")
|
|
34
|
+
? join(homedir(), cfg.memoryDir.slice(1))
|
|
35
|
+
: cfg.memoryDir;
|
|
36
|
+
mkdirSync(memoryDir, { recursive: true });
|
|
37
|
+
|
|
38
|
+
const store = createStore(join(memoryDir, "memory.db"));
|
|
39
|
+
const mirror = createMirror(memoryDir);
|
|
40
|
+
const service = createService({ store, mirror, config: cfg });
|
|
41
|
+
|
|
42
|
+
// User-configurable settings (profile, rules) and custom commands share the
|
|
43
|
+
// same SQLite file but live in dedicated tables, isolated from memories.
|
|
44
|
+
const settings = createSettings(store.db);
|
|
45
|
+
|
|
46
|
+
// Semantic pipeline: a local/ollama embedder when configured, otherwise the
|
|
47
|
+
// legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
|
|
48
|
+
// the store's embedding column and tracks the active model fingerprint. A
|
|
49
|
+
// slow embedder init (model download) never blocks plugin boot — failures
|
|
50
|
+
// degrade to keyword search.
|
|
51
|
+
const vectorIndex = createVectorIndex({ store, logger: ctx.logger });
|
|
52
|
+
service.setVectorIndex(vectorIndex);
|
|
53
|
+
|
|
54
|
+
let embedder = null;
|
|
55
|
+
let reranker = null;
|
|
56
|
+
if (cfg.embedProvider === "openai") {
|
|
57
|
+
embedder = createEmbedder({ store, settings, logger: ctx.logger });
|
|
58
|
+
service.setEmbedder(embedder);
|
|
59
|
+
} else {
|
|
60
|
+
try {
|
|
61
|
+
embedder = createEmbedderByProvider(cfg.embedProvider, {
|
|
62
|
+
model: cfg.embedProvider === "ollama" ? cfg.ollamaModel : cfg.localEmbedModel,
|
|
63
|
+
dimension: cfg.localEmbedDimension,
|
|
64
|
+
device: cfg.localEmbedDevice,
|
|
65
|
+
batchSize: cfg.localEmbedBatchSize,
|
|
66
|
+
cacheDir: cfg.embedModelCacheDir,
|
|
67
|
+
baseUrl: cfg.ollamaBaseUrl,
|
|
68
|
+
logger: ctx.logger
|
|
69
|
+
});
|
|
70
|
+
service.setEmbedder(embedder);
|
|
71
|
+
embedder.init().catch((error) => {
|
|
72
|
+
ctx.logger?.warn?.(`[dsh-mneme] embedder init failed, search degrades to keyword: ${String(error)}`);
|
|
73
|
+
service.setEmbedder(null);
|
|
74
|
+
});
|
|
75
|
+
} catch (error) {
|
|
76
|
+
ctx.logger?.warn?.(`[dsh-mneme] embedder unavailable, search degrades to keyword: ${String(error)}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Cross-encoder rerank over recall candidates. Best-effort: a failed model
|
|
81
|
+
// load only disables reranking, never search itself.
|
|
82
|
+
if (cfg.rerankEnabled && cfg.rerankProvider === "local") {
|
|
83
|
+
try {
|
|
84
|
+
reranker = new LocalReranker({
|
|
85
|
+
model: cfg.rerankModel,
|
|
86
|
+
batchSize: cfg.rerankBatchSize,
|
|
87
|
+
maxCandidates: cfg.rerankMaxCandidates,
|
|
88
|
+
scoreThreshold: cfg.rerankScoreThreshold,
|
|
89
|
+
device: cfg.localEmbedDevice,
|
|
90
|
+
cacheDir: cfg.embedModelCacheDir,
|
|
91
|
+
logger: ctx.logger
|
|
92
|
+
});
|
|
93
|
+
service.setReranker(reranker);
|
|
94
|
+
reranker.init().catch((error) => {
|
|
95
|
+
ctx.logger?.warn?.(`[dsh-mneme] reranker init failed, rerank disabled: ${String(error)}`);
|
|
96
|
+
service.setReranker(null);
|
|
97
|
+
});
|
|
98
|
+
} catch (error) {
|
|
99
|
+
ctx.logger?.warn?.(`[dsh-mneme] reranker unavailable, rerank disabled: ${String(error)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Custom commands: register persisted commands into the DSH command registry
|
|
104
|
+
// on boot; add/remove re-register live through the API.
|
|
105
|
+
let commands = null;
|
|
106
|
+
if (ctx.commands) {
|
|
107
|
+
commands = createCommandManager({ ctx, settings, logger: ctx.logger });
|
|
108
|
+
commands.sync();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Human edits in mirror files win on every sync; merge them back first.
|
|
112
|
+
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
113
|
+
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
114
|
+
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
115
|
+
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
116
|
+
const humanEdits = new Map();
|
|
117
|
+
for (const type of Object.keys(TYPE_FILE)) {
|
|
118
|
+
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
119
|
+
}
|
|
120
|
+
for (const [type, edits] of humanEdits) {
|
|
121
|
+
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Dream scheduler: automatic consolidation + summary runs, triggered by
|
|
125
|
+
// store growth. Writes through the service fire the dream hook, which asks
|
|
126
|
+
// the scheduler to (re)schedule a run once absolute and since-last-run
|
|
127
|
+
// thresholds are both exceeded. onRun is deferred through `dream` so the
|
|
128
|
+
// closure sees the assigned scheduler; the null guard keeps a run safe even
|
|
129
|
+
// if the hook fires before assignment or after dispose.
|
|
130
|
+
let dream = null;
|
|
131
|
+
if (cfg.autoDream) {
|
|
132
|
+
dream = createDreamScheduler({
|
|
133
|
+
thresholdCount: cfg.dreamThresholdCount,
|
|
134
|
+
thresholdChars: cfg.dreamThresholdChars,
|
|
135
|
+
delayMs: cfg.dreamDelayMs,
|
|
136
|
+
logger: ctx.logger,
|
|
137
|
+
semantic: { embedder, vectorIndex },
|
|
138
|
+
onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
|
|
139
|
+
});
|
|
140
|
+
service.setDreamHook(() => dream.maybeSchedule(service));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const disposers = [];
|
|
144
|
+
|
|
145
|
+
ctx.inject(["systemPrompt"], (promptCtx) => {
|
|
146
|
+
if (cfg.autoInject) disposers.push(createInjector(promptCtx, service, settings, cfg));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
ctx.inject(["tools"], (toolsCtx) => {
|
|
150
|
+
disposers.push(createTools(toolsCtx, service, cfg, embedder));
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const summarizer = createSummarizer(ctx, service, cfg);
|
|
154
|
+
disposers.push(summarizer.dispose);
|
|
155
|
+
|
|
156
|
+
if (ctx.webServer) {
|
|
157
|
+
const api = createApi(ctx, service, settings, commands ?? {
|
|
158
|
+
add: () => { throw new Error("commands unavailable"); },
|
|
159
|
+
remove: () => false,
|
|
160
|
+
list: () => []
|
|
161
|
+
}, embedder, { vectorIndex, reranker });
|
|
162
|
+
disposers.push(api.dispose);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Async disposer: cordis awaits the returned promise on unload (runDisposable),
|
|
166
|
+
// so an in-flight dream run is allowed to finish before the SQLite store is
|
|
167
|
+
// closed — dream.dispose() resolves only after its current run settles.
|
|
168
|
+
return async () => {
|
|
169
|
+
for (const dispose of disposers) {
|
|
170
|
+
if (typeof dispose === "function") dispose();
|
|
171
|
+
}
|
|
172
|
+
commands?.dispose();
|
|
173
|
+
if (dream) await dream.dispose();
|
|
174
|
+
store.close();
|
|
175
|
+
};
|
|
176
|
+
};
|
package/src/inject.js
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
|
-
export function createInjector(ctx, service, settings, config) {
|
|
2
|
-
const maxItems = config.maxInjectedItems ?? 5;
|
|
3
|
-
const threshold = config.importanceThreshold ?? 3;
|
|
4
|
-
|
|
5
|
-
function render(candidates) {
|
|
6
|
-
if (!candidates.length) return "";
|
|
7
|
-
const lines = ["[记忆库] 来自 dsh-mneme 的跨会话记忆(用户偏好与高优先级项目/决策):"];
|
|
8
|
-
for (const m of candidates) {
|
|
9
|
-
lines.push(`- [${m.type}] ${m.title}(重要性 ${m.importance}):${m.content}`);
|
|
10
|
-
}
|
|
11
|
-
return lines.join("\n");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// User profile + rules: injected ahead of the memory block because they are
|
|
15
|
-
// always-relevant instructions the agent should follow every turn.
|
|
16
|
-
function renderUserSettings() {
|
|
17
|
-
const profile = settings.getProfile().trim();
|
|
18
|
-
const rules = settings.getRules();
|
|
19
|
-
if (!profile && !rules.length) return "";
|
|
20
|
-
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
21
|
-
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
22
|
-
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
23
|
-
return lines.join("\n");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const disposers = [
|
|
27
|
-
ctx.systemPrompt.context({
|
|
28
|
-
name: "memory",
|
|
29
|
-
order: 90,
|
|
30
|
-
text: () => {
|
|
31
|
-
const candidates = service.injectCandidates({ maxItems, threshold });
|
|
32
|
-
return render(candidates);
|
|
33
|
-
}
|
|
34
|
-
}),
|
|
35
|
-
ctx.systemPrompt.context({
|
|
36
|
-
name: "user-settings",
|
|
37
|
-
order: 85,
|
|
38
|
-
text: renderUserSettings
|
|
39
|
-
})
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
return () => {
|
|
43
|
-
for (const dispose of disposers) {
|
|
44
|
-
if (typeof dispose === "function") dispose();
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
}
|
|
1
|
+
export function createInjector(ctx, service, settings, config) {
|
|
2
|
+
const maxItems = config.maxInjectedItems ?? 5;
|
|
3
|
+
const threshold = config.importanceThreshold ?? 3;
|
|
4
|
+
|
|
5
|
+
function render(candidates) {
|
|
6
|
+
if (!candidates.length) return "";
|
|
7
|
+
const lines = ["[记忆库] 来自 dsh-mneme 的跨会话记忆(用户偏好与高优先级项目/决策):"];
|
|
8
|
+
for (const m of candidates) {
|
|
9
|
+
lines.push(`- [${m.type}] ${m.title}(重要性 ${m.importance}):${m.content}`);
|
|
10
|
+
}
|
|
11
|
+
return lines.join("\n");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// User profile + rules: injected ahead of the memory block because they are
|
|
15
|
+
// always-relevant instructions the agent should follow every turn.
|
|
16
|
+
function renderUserSettings() {
|
|
17
|
+
const profile = settings.getProfile().trim();
|
|
18
|
+
const rules = settings.getRules();
|
|
19
|
+
if (!profile && !rules.length) return "";
|
|
20
|
+
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
21
|
+
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
22
|
+
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
23
|
+
return lines.join("\n");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const disposers = [
|
|
27
|
+
ctx.systemPrompt.context({
|
|
28
|
+
name: "memory",
|
|
29
|
+
order: 90,
|
|
30
|
+
text: () => {
|
|
31
|
+
const candidates = service.injectCandidates({ maxItems, threshold });
|
|
32
|
+
return render(candidates);
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
ctx.systemPrompt.context({
|
|
36
|
+
name: "user-settings",
|
|
37
|
+
order: 85,
|
|
38
|
+
text: renderUserSettings
|
|
39
|
+
})
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
for (const dispose of disposers) {
|
|
44
|
+
if (typeof dispose === "function") dispose();
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|