dsh-layered-memory 0.5.3
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 -0
- package/README.md +186 -0
- package/assets/readme/hero.svg +58 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +1066 -0
- package/dist/config.d.ts +263 -0
- package/dist/config.js +65 -0
- package/dist/hooks/capture.d.ts +16 -0
- package/dist/hooks/capture.js +160 -0
- package/dist/hooks/recall.d.ts +31 -0
- package/dist/hooks/recall.js +201 -0
- package/dist/index.d.ts +190 -0
- package/dist/index.js +234 -0
- package/dist/llm.d.ts +32 -0
- package/dist/llm.js +128 -0
- package/dist/pipeline/l1.d.ts +19 -0
- package/dist/pipeline/l1.js +226 -0
- package/dist/pipeline/l2.d.ts +13 -0
- package/dist/pipeline/l2.js +77 -0
- package/dist/pipeline/l3.d.ts +15 -0
- package/dist/pipeline/l3.js +72 -0
- package/dist/pipeline/runner.d.ts +44 -0
- package/dist/pipeline/runner.js +132 -0
- package/dist/prompts/l1-dedup.d.ts +20 -0
- package/dist/prompts/l1-dedup.js +244 -0
- package/dist/prompts/l1-extraction.d.ts +18 -0
- package/dist/prompts/l1-extraction.js +434 -0
- package/dist/prompts/persona.d.ts +23 -0
- package/dist/prompts/persona.js +234 -0
- package/dist/prompts/scene.d.ts +33 -0
- package/dist/prompts/scene.js +408 -0
- package/dist/settings.d.ts +27 -0
- package/dist/settings.js +73 -0
- package/dist/stats.d.ts +46 -0
- package/dist/stats.js +234 -0
- package/dist/store/bm25.d.ts +19 -0
- package/dist/store/bm25.js +62 -0
- package/dist/store/embedding.d.ts +68 -0
- package/dist/store/embedding.js +133 -0
- package/dist/store/io.d.ts +13 -0
- package/dist/store/io.js +75 -0
- package/dist/store/l0.d.ts +28 -0
- package/dist/store/l0.js +148 -0
- package/dist/store/l1.d.ts +70 -0
- package/dist/store/l1.js +239 -0
- package/dist/store/persona.d.ts +15 -0
- package/dist/store/persona.js +60 -0
- package/dist/store/scenes.d.ts +23 -0
- package/dist/store/scenes.js +150 -0
- package/dist/store/search-utils.d.ts +22 -0
- package/dist/store/search-utils.js +71 -0
- package/dist/store/session-modes.d.ts +24 -0
- package/dist/store/session-modes.js +102 -0
- package/dist/store/sqlite.d.ts +121 -0
- package/dist/store/sqlite.js +922 -0
- package/dist/store/state.d.ts +35 -0
- package/dist/store/state.js +62 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +169 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.js +7 -0
- package/dist/util/filelog.d.ts +4 -0
- package/dist/util/filelog.js +43 -0
- package/dist/util/sanitize.d.ts +10 -0
- package/dist/util/sanitize.js +66 -0
- package/dist/util/text.d.ts +11 -0
- package/dist/util/text.js +43 -0
- package/package.json +80 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { blocksToText } from '../util/text.js';
|
|
2
|
+
const PROFILE_TTL = 60_000;
|
|
3
|
+
const MEMORY_TOOLS_GUIDE = `<memory-tools-guide>
|
|
4
|
+
## 记忆工具调用指南
|
|
5
|
+
|
|
6
|
+
当上方注入的记忆片段不足以回答用户问题时,可主动调用以下工具获取更多信息:
|
|
7
|
+
|
|
8
|
+
- **memory_search**:搜索结构化记忆(L1),适用于回忆用户偏好、历史事件、项目事实、任务、规则等。
|
|
9
|
+
- **conversation_search**:搜索原始对话(L0),适用于查找具体消息原文、时间线、上下文细节。
|
|
10
|
+
- **memory_read_scene**:读取记忆文件详情(L2 场景块,如场景目录下的 .md 文件;也可读 persona.md)。
|
|
11
|
+
|
|
12
|
+
### ⚠️ 调用次数限制
|
|
13
|
+
每轮对话中,memory_search 和 conversation_search **合计最多调用 3 次**。
|
|
14
|
+
- 首次搜索无结果时,可换关键词或换工具重试,但总调用次数不要超过 3 次。
|
|
15
|
+
- 若 3 次搜索后仍无结果,说明该信息不在记忆中,请直接根据已有信息回复用户。
|
|
16
|
+
</memory-tools-guide>`;
|
|
17
|
+
export function registerRecall(ctx, cfg, stores, logger, live, modes) {
|
|
18
|
+
const l1Cache = new Map();
|
|
19
|
+
// 画像/场景导航按族缓存(分族隔离:注入时按会话档位选族)
|
|
20
|
+
const profileCache = {
|
|
21
|
+
chat: { persona: '', nav: '' },
|
|
22
|
+
work: { persona: '', nav: '' },
|
|
23
|
+
};
|
|
24
|
+
const refreshProfile = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const [chat, work] = await Promise.all([
|
|
27
|
+
loadProfileParts(stores, cfg, 'chat'),
|
|
28
|
+
loadProfileParts(stores, cfg, 'work'),
|
|
29
|
+
]);
|
|
30
|
+
profileCache.chat = chat;
|
|
31
|
+
profileCache.work = work;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
logger.warn(`[memory] 画像/场景缓存刷新失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// 初始刷新 + 定时刷新(TTL)
|
|
38
|
+
void refreshProfile();
|
|
39
|
+
ctx.effect(() => {
|
|
40
|
+
const timer = setInterval(() => void refreshProfile(), PROFILE_TTL);
|
|
41
|
+
return () => clearInterval(timer);
|
|
42
|
+
});
|
|
43
|
+
const invalidateProfile = () => {
|
|
44
|
+
void refreshProfile();
|
|
45
|
+
};
|
|
46
|
+
// agent 销毁时清掉召回缓存槽
|
|
47
|
+
ctx.on('agent/disposed', (payload) => {
|
|
48
|
+
l1Cache.delete(payload.agent.id);
|
|
49
|
+
});
|
|
50
|
+
// ── 1. pre-step:检索并缓存(按会话档位过滤族:纯档只查本族,auto 不过滤,off 跳过) ──
|
|
51
|
+
if (cfg.recall.enabled) {
|
|
52
|
+
ctx.on('agent/pre-step', async (payload, next) => {
|
|
53
|
+
try {
|
|
54
|
+
const s = live.get();
|
|
55
|
+
const mode = modes.get(payload.agent.id);
|
|
56
|
+
if (!s.enabled || !s.recall || mode === 'off') {
|
|
57
|
+
l1Cache.set(payload.agent.id, []);
|
|
58
|
+
return next();
|
|
59
|
+
}
|
|
60
|
+
const query = payload.messages
|
|
61
|
+
.map((m) => blocksToText(m.content))
|
|
62
|
+
.join(' ')
|
|
63
|
+
.trim();
|
|
64
|
+
if (query) {
|
|
65
|
+
const hits = await stores.l1.search(query, cfg.recall.maxResults, {
|
|
66
|
+
scoreThreshold: cfg.recall.scoreThreshold,
|
|
67
|
+
family: mode === 'auto' ? undefined : mode,
|
|
68
|
+
});
|
|
69
|
+
l1Cache.set(payload.agent.id, hits);
|
|
70
|
+
if (hits.length > 0) {
|
|
71
|
+
logger.info(`[memory] 召回命中 ${hits.length} 条 L1(mode=${mode},query="${query.slice(0, 30).replace(/\n/g, ' ')}…",agent=${payload.agent.id})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
logger.warn(`[memory] 召回检索失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
return next();
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// ── 2. agent 作用域上下文 provider ──
|
|
82
|
+
// 插件可能在默认 agent 创建之后才加载(组合顺序由依赖决定),
|
|
83
|
+
// 因此除了监听 agent/created,还要给已存在的 agent 补注册。
|
|
84
|
+
const registered = new WeakSet();
|
|
85
|
+
// context() 的 disposer 必须挂到插件自身生命周期:agent.ctx 比插件实例活得久,
|
|
86
|
+
// 不主动清理会导致热重载后旧注册泄漏、新实例撞名("already registered")
|
|
87
|
+
const contextDisposers = [];
|
|
88
|
+
const registerForAgent = (agent) => {
|
|
89
|
+
if (registered.has(agent))
|
|
90
|
+
return;
|
|
91
|
+
registered.add(agent);
|
|
92
|
+
try {
|
|
93
|
+
contextDisposers.push(agent.ctx.systemPrompt.context({
|
|
94
|
+
name: 'memory:recall',
|
|
95
|
+
order: 500,
|
|
96
|
+
text: () => {
|
|
97
|
+
const s = live.get();
|
|
98
|
+
if (!s.enabled || !s.recall)
|
|
99
|
+
return '';
|
|
100
|
+
const hits = l1Cache.get(agent.id) ?? [];
|
|
101
|
+
return formatL1Memories(hits);
|
|
102
|
+
},
|
|
103
|
+
}), agent.ctx.systemPrompt.context({
|
|
104
|
+
name: 'memory:profile',
|
|
105
|
+
order: 510,
|
|
106
|
+
text: () => {
|
|
107
|
+
const s = live.get();
|
|
108
|
+
if (!s.enabled || !s.recall)
|
|
109
|
+
return '';
|
|
110
|
+
const mode = modes.get(agent.id);
|
|
111
|
+
if (mode === 'off')
|
|
112
|
+
return '';
|
|
113
|
+
// auto 档:两族按类别归组(画像/导航各一个标签,域内 <domain> 分块);纯档:单族原格式
|
|
114
|
+
const body = mode === 'auto'
|
|
115
|
+
? formatProfileAuto(profileCache.chat, profileCache.work)
|
|
116
|
+
: formatProfileSingle(profileCache[mode]);
|
|
117
|
+
if (!body)
|
|
118
|
+
return MEMORY_TOOLS_GUIDE;
|
|
119
|
+
return `${body}\n\n${MEMORY_TOOLS_GUIDE}`;
|
|
120
|
+
},
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.warn(`[memory] 召回上下文注册失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const agents = ctx.get('agents');
|
|
128
|
+
if (agents) {
|
|
129
|
+
for (const agent of agents.list())
|
|
130
|
+
registerForAgent(agent);
|
|
131
|
+
}
|
|
132
|
+
ctx.on('agent/created', (payload) => {
|
|
133
|
+
registerForAgent(payload.agent);
|
|
134
|
+
});
|
|
135
|
+
ctx.effect(() => () => {
|
|
136
|
+
for (const dispose of contextDisposers.splice(0)) {
|
|
137
|
+
try {
|
|
138
|
+
dispose();
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
/* agent 可能已先一步销毁 */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return { invalidateProfile };
|
|
146
|
+
}
|
|
147
|
+
function formatL1Memories(items) {
|
|
148
|
+
if (items.length === 0)
|
|
149
|
+
return '';
|
|
150
|
+
const lines = ['<relevant-memories>', ''];
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
// [type|scene] 标签 + 内容(官方召回行格式)
|
|
153
|
+
const tag = item.scene_name ? `${item.type}|${item.scene_name}` : item.type;
|
|
154
|
+
lines.push(`- [${tag}] ${item.content}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push('', '</relevant-memories>');
|
|
157
|
+
return lines.join('\n');
|
|
158
|
+
}
|
|
159
|
+
/** auto 档 <user-persona> 内的域说明:让模型理解分块结构与两域的独立性。 */
|
|
160
|
+
const DOMAIN_HINT = '以下内容按记忆域分块:chat=用户个人画像(User Narrative Profile),work=团队工作准则(Team Operating Doctrine)。' +
|
|
161
|
+
'两域独立蒸馏与更新,请按当前对话语境参考对应域,不要把一域的内容当作另一域的事实。';
|
|
162
|
+
function wrapDomain(family, content) {
|
|
163
|
+
const label = family === 'chat' ? '用户个人画像' : '团队工作准则';
|
|
164
|
+
return `<domain family="${family}" label="${label}">\n${content.trim()}\n</domain>`;
|
|
165
|
+
}
|
|
166
|
+
/** 纯档注入:单族画像 + 场景导航(沿用原有格式)。 */
|
|
167
|
+
function formatProfileSingle(parts) {
|
|
168
|
+
const segments = [];
|
|
169
|
+
if (parts.persona)
|
|
170
|
+
segments.push(`<user-persona>\n${parts.persona}\n</user-persona>`);
|
|
171
|
+
if (parts.nav)
|
|
172
|
+
segments.push(`<scene-navigation>\n${parts.nav}\n</scene-navigation>`);
|
|
173
|
+
return segments.join('\n\n');
|
|
174
|
+
}
|
|
175
|
+
/** auto 档注入:两族按类别归组——画像共用一个 <user-persona>、导航共用一个 <scene-navigation>,域内 <domain> 分块。 */
|
|
176
|
+
function formatProfileAuto(chat, work) {
|
|
177
|
+
const segments = [];
|
|
178
|
+
const personas = [
|
|
179
|
+
['chat', chat.persona],
|
|
180
|
+
['work', work.persona],
|
|
181
|
+
];
|
|
182
|
+
const personaBlocks = personas.filter(([, p]) => p.trim()).map(([f, p]) => wrapDomain(f, p));
|
|
183
|
+
if (personaBlocks.length > 0) {
|
|
184
|
+
segments.push(`<user-persona>\n${DOMAIN_HINT}\n\n${personaBlocks.join('\n\n')}\n</user-persona>`);
|
|
185
|
+
}
|
|
186
|
+
const navs = [
|
|
187
|
+
['chat', chat.nav],
|
|
188
|
+
['work', work.nav],
|
|
189
|
+
];
|
|
190
|
+
const navBlocks = navs.filter(([, n]) => n.trim()).map(([f, n]) => wrapDomain(f, n));
|
|
191
|
+
if (navBlocks.length > 0) {
|
|
192
|
+
segments.push(`<scene-navigation>\n${navBlocks.join('\n\n')}\n</scene-navigation>`);
|
|
193
|
+
}
|
|
194
|
+
// 注意:工具指南由注入侧统一附加一次(auto 档不重复)
|
|
195
|
+
return segments.join('\n\n');
|
|
196
|
+
}
|
|
197
|
+
async function loadProfileParts(stores, cfg, family) {
|
|
198
|
+
const persona = cfg.recall.includePersona ? ((await stores.persona[family].read()) ?? '') : '';
|
|
199
|
+
const nav = cfg.recall.includeSceneNav ? ((await stores.scenes[family].navigation()) ?? '').trim() : '';
|
|
200
|
+
return { persona, nav };
|
|
201
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import { type MemoryConfig } from './config.js';
|
|
3
|
+
export declare const name = "dsh-memory-plugin";
|
|
4
|
+
/** 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。 */
|
|
5
|
+
export declare const inject: string[];
|
|
6
|
+
/**
|
|
7
|
+
* 插件配置 schema。导出名必须是 `Config`——cordis 运行时只读 plugin.Config
|
|
8
|
+
* (Standard Schema 接口)做校验与默认值填充;导出 `schema` 会被静默忽略,
|
|
9
|
+
* 导致 config 里嵌套对象为 undefined、apply 抛错、fiber FAILED 拖垮宿主启动。
|
|
10
|
+
*/
|
|
11
|
+
export declare const Config: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
12
|
+
dataDir: import("@deepseek-ai/schemastery").default<string, string>;
|
|
13
|
+
family: import("@deepseek-ai/schemastery").default<"chat" | "work" | "auto", "chat" | "work" | "auto">;
|
|
14
|
+
capture: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
15
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
16
|
+
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
17
|
+
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
18
|
+
}>, Schemastery.ObjectT<{
|
|
19
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
20
|
+
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
21
|
+
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
22
|
+
}>>;
|
|
23
|
+
extract: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
24
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
25
|
+
minMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
26
|
+
backgroundMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
27
|
+
candidatePool: import("@deepseek-ai/schemastery").default<number, number>;
|
|
28
|
+
}>, Schemastery.ObjectT<{
|
|
29
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
30
|
+
minMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
31
|
+
backgroundMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
32
|
+
candidatePool: import("@deepseek-ai/schemastery").default<number, number>;
|
|
33
|
+
}>>;
|
|
34
|
+
l2: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
35
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
36
|
+
minNewMemories: import("@deepseek-ai/schemastery").default<number, number>;
|
|
37
|
+
maxScenes: import("@deepseek-ai/schemastery").default<number, number>;
|
|
38
|
+
sceneContextLimit: import("@deepseek-ai/schemastery").default<number, number>;
|
|
39
|
+
}>, Schemastery.ObjectT<{
|
|
40
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
41
|
+
minNewMemories: import("@deepseek-ai/schemastery").default<number, number>;
|
|
42
|
+
maxScenes: import("@deepseek-ai/schemastery").default<number, number>;
|
|
43
|
+
sceneContextLimit: import("@deepseek-ai/schemastery").default<number, number>;
|
|
44
|
+
}>>;
|
|
45
|
+
l3: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
46
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
47
|
+
interval: import("@deepseek-ai/schemastery").default<number, number>;
|
|
48
|
+
}>, Schemastery.ObjectT<{
|
|
49
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
50
|
+
interval: import("@deepseek-ai/schemastery").default<number, number>;
|
|
51
|
+
}>>;
|
|
52
|
+
recall: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
53
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
54
|
+
maxResults: import("@deepseek-ai/schemastery").default<number, number>;
|
|
55
|
+
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
56
|
+
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
57
|
+
strategy: import("@deepseek-ai/schemastery").default<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
58
|
+
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
59
|
+
}>, Schemastery.ObjectT<{
|
|
60
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
61
|
+
maxResults: import("@deepseek-ai/schemastery").default<number, number>;
|
|
62
|
+
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
63
|
+
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
64
|
+
strategy: import("@deepseek-ai/schemastery").default<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
65
|
+
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
66
|
+
}>>;
|
|
67
|
+
embedding: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
68
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
69
|
+
baseUrl: import("@deepseek-ai/schemastery").default<string, string>;
|
|
70
|
+
apiKey: import("@deepseek-ai/schemastery").default<string, string>;
|
|
71
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
72
|
+
dimensions: import("@deepseek-ai/schemastery").default<number, number>;
|
|
73
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
74
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
75
|
+
}>, Schemastery.ObjectT<{
|
|
76
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
77
|
+
baseUrl: import("@deepseek-ai/schemastery").default<string, string>;
|
|
78
|
+
apiKey: import("@deepseek-ai/schemastery").default<string, string>;
|
|
79
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
80
|
+
dimensions: import("@deepseek-ai/schemastery").default<number, number>;
|
|
81
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
82
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
83
|
+
}>>;
|
|
84
|
+
llm: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
85
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
86
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
87
|
+
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
88
|
+
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
89
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
90
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
91
|
+
}>, Schemastery.ObjectT<{
|
|
92
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
93
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
94
|
+
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
95
|
+
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
96
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
97
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
98
|
+
}>>;
|
|
99
|
+
tools: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
100
|
+
}>, Schemastery.ObjectT<{
|
|
101
|
+
dataDir: import("@deepseek-ai/schemastery").default<string, string>;
|
|
102
|
+
family: import("@deepseek-ai/schemastery").default<"chat" | "work" | "auto", "chat" | "work" | "auto">;
|
|
103
|
+
capture: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
104
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
105
|
+
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
106
|
+
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
107
|
+
}>, Schemastery.ObjectT<{
|
|
108
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
109
|
+
stripCodeBlocks: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
110
|
+
maxMessageChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
111
|
+
}>>;
|
|
112
|
+
extract: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
113
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
114
|
+
minMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
115
|
+
backgroundMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
116
|
+
candidatePool: import("@deepseek-ai/schemastery").default<number, number>;
|
|
117
|
+
}>, Schemastery.ObjectT<{
|
|
118
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
119
|
+
minMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
120
|
+
backgroundMessages: import("@deepseek-ai/schemastery").default<number, number>;
|
|
121
|
+
candidatePool: import("@deepseek-ai/schemastery").default<number, number>;
|
|
122
|
+
}>>;
|
|
123
|
+
l2: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
124
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
125
|
+
minNewMemories: import("@deepseek-ai/schemastery").default<number, number>;
|
|
126
|
+
maxScenes: import("@deepseek-ai/schemastery").default<number, number>;
|
|
127
|
+
sceneContextLimit: import("@deepseek-ai/schemastery").default<number, number>;
|
|
128
|
+
}>, Schemastery.ObjectT<{
|
|
129
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
130
|
+
minNewMemories: import("@deepseek-ai/schemastery").default<number, number>;
|
|
131
|
+
maxScenes: import("@deepseek-ai/schemastery").default<number, number>;
|
|
132
|
+
sceneContextLimit: import("@deepseek-ai/schemastery").default<number, number>;
|
|
133
|
+
}>>;
|
|
134
|
+
l3: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
135
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
136
|
+
interval: import("@deepseek-ai/schemastery").default<number, number>;
|
|
137
|
+
}>, Schemastery.ObjectT<{
|
|
138
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
139
|
+
interval: import("@deepseek-ai/schemastery").default<number, number>;
|
|
140
|
+
}>>;
|
|
141
|
+
recall: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
142
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
143
|
+
maxResults: import("@deepseek-ai/schemastery").default<number, number>;
|
|
144
|
+
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
145
|
+
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
146
|
+
strategy: import("@deepseek-ai/schemastery").default<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
147
|
+
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
148
|
+
}>, Schemastery.ObjectT<{
|
|
149
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
150
|
+
maxResults: import("@deepseek-ai/schemastery").default<number, number>;
|
|
151
|
+
includePersona: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
152
|
+
includeSceneNav: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
153
|
+
strategy: import("@deepseek-ai/schemastery").default<"keyword" | "embedding" | "hybrid", "keyword" | "embedding" | "hybrid">;
|
|
154
|
+
scoreThreshold: import("@deepseek-ai/schemastery").default<number, number>;
|
|
155
|
+
}>>;
|
|
156
|
+
embedding: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
157
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
158
|
+
baseUrl: import("@deepseek-ai/schemastery").default<string, string>;
|
|
159
|
+
apiKey: import("@deepseek-ai/schemastery").default<string, string>;
|
|
160
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
161
|
+
dimensions: import("@deepseek-ai/schemastery").default<number, number>;
|
|
162
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
163
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
164
|
+
}>, Schemastery.ObjectT<{
|
|
165
|
+
enabled: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
166
|
+
baseUrl: import("@deepseek-ai/schemastery").default<string, string>;
|
|
167
|
+
apiKey: import("@deepseek-ai/schemastery").default<string, string>;
|
|
168
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
169
|
+
dimensions: import("@deepseek-ai/schemastery").default<number, number>;
|
|
170
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
171
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
172
|
+
}>>;
|
|
173
|
+
llm: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<{
|
|
174
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
175
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
176
|
+
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
177
|
+
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
178
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
179
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
180
|
+
}>, Schemastery.ObjectT<{
|
|
181
|
+
provider: import("@deepseek-ai/schemastery").default<string, string>;
|
|
182
|
+
model: import("@deepseek-ai/schemastery").default<string, string>;
|
|
183
|
+
maxTokens: import("@deepseek-ai/schemastery").default<number, number>;
|
|
184
|
+
temperature: import("@deepseek-ai/schemastery").default<number, number>;
|
|
185
|
+
maxInputChars: import("@deepseek-ai/schemastery").default<number, number>;
|
|
186
|
+
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
187
|
+
}>>;
|
|
188
|
+
tools: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
189
|
+
}>>;
|
|
190
|
+
export declare function apply(ctx: Context, config: MemoryConfig): Promise<void>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-memory-plugin — L0~L3 分层蒸馏记忆插件(持久组合插件)。
|
|
3
|
+
*
|
|
4
|
+
* 功能:
|
|
5
|
+
* - L0:订阅 session/event,按轮次捕获原始对话,双写 JSONL 事实源 + SQLite 检索库;
|
|
6
|
+
* - L1:LLM 抽取原子记忆(情境切分 + 记忆提取)+ 冲突检测去重 + FTS/向量/混合检索;
|
|
7
|
+
* - L2:LLM 把新记忆整合为场景块(Markdown,META 块 + 热度管理 + 场景导航);
|
|
8
|
+
* - L3:LLM 从变化场景蒸馏 persona.md(chat=用户画像 / work=Team Operating Doctrine);
|
|
9
|
+
* - 召回:agent/pre-step 检索 L1,agent 作用域上下文注入画像/场景导航/工具指南;
|
|
10
|
+
* - 工具:memory_search / conversation_search / memory_read_scene。
|
|
11
|
+
*
|
|
12
|
+
* 存储架构(对齐 MemoryCore 官方双写设计):
|
|
13
|
+
* - JSONL 追加文件(conversations/、records/ 按天分片)= 备份/恢复事实源;
|
|
14
|
+
* - memory.db(node:sqlite + WAL + FTS5 BM25 + sqlite-vec 余弦向量)= 主检索引擎。
|
|
15
|
+
*
|
|
16
|
+
* 移植自 MemoryCore(TencentDB Agent Memory)的 L0~L3 设计,Prompt 原样保留,
|
|
17
|
+
* 仅把"LLM 操作文件"的 L2/L3 流程适配为"LLM 输出、工程侧执行"。
|
|
18
|
+
*/
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { memorySchema, resolveDataDir } from './config.js';
|
|
21
|
+
import { registerCapture } from './hooks/capture.js';
|
|
22
|
+
import { registerRecall } from './hooks/recall.js';
|
|
23
|
+
import { MemoryRunner } from './pipeline/runner.js';
|
|
24
|
+
import { registerMemoryRpc, PLUGIN_VERSION } from './stats.js';
|
|
25
|
+
import { registerLiveSettings } from './settings.js';
|
|
26
|
+
import { NoopEmbeddingService, RemoteEmbeddingService, } from './store/embedding.js';
|
|
27
|
+
import { ensureDir } from './store/io.js';
|
|
28
|
+
import { L0Store } from './store/l0.js';
|
|
29
|
+
import { L1Store } from './store/l1.js';
|
|
30
|
+
import { PersonaStore } from './store/persona.js';
|
|
31
|
+
import { MemoryDb } from './store/sqlite.js';
|
|
32
|
+
import { SceneStore } from './store/scenes.js';
|
|
33
|
+
import { SessionModeStore } from './store/session-modes.js';
|
|
34
|
+
import { StateStore } from './store/state.js';
|
|
35
|
+
import { registerMemoryTools } from './tools/index.js';
|
|
36
|
+
import { errDetail, withFileLog } from './util/filelog.js';
|
|
37
|
+
import { resolveModelRoute } from './llm.js';
|
|
38
|
+
export const name = 'dsh-memory-plugin';
|
|
39
|
+
/** 硬依赖:蒸馏要用 llm,工具注册要用 tools,召回注入要用 systemPrompt。 */
|
|
40
|
+
export const inject = ['llm', 'tools', 'systemPrompt'];
|
|
41
|
+
/**
|
|
42
|
+
* 插件配置 schema。导出名必须是 `Config`——cordis 运行时只读 plugin.Config
|
|
43
|
+
* (Standard Schema 接口)做校验与默认值填充;导出 `schema` 会被静默忽略,
|
|
44
|
+
* 导致 config 里嵌套对象为 undefined、apply 抛错、fiber FAILED 拖垮宿主启动。
|
|
45
|
+
*/
|
|
46
|
+
export const Config = memorySchema;
|
|
47
|
+
/** 按配置构造 embedding 服务:未启用或配置不完整 → Noop(纯 FTS 运行)。 */
|
|
48
|
+
function createEmbeddingService(cfg, logger) {
|
|
49
|
+
if (!cfg.embedding.enabled)
|
|
50
|
+
return new NoopEmbeddingService();
|
|
51
|
+
const { baseUrl, apiKey, model, dimensions } = cfg.embedding;
|
|
52
|
+
if (!baseUrl || !apiKey || !model || dimensions <= 0) {
|
|
53
|
+
logger.error('[memory] embedding.enabled=true 但 baseUrl/apiKey/model/dimensions 未配置完整,向量检索停用(纯 FTS 运行)');
|
|
54
|
+
return new NoopEmbeddingService();
|
|
55
|
+
}
|
|
56
|
+
return new RemoteEmbeddingService({
|
|
57
|
+
baseUrl,
|
|
58
|
+
apiKey,
|
|
59
|
+
model,
|
|
60
|
+
dimensions,
|
|
61
|
+
maxInputChars: cfg.embedding.maxInputChars,
|
|
62
|
+
timeoutMs: cfg.embedding.timeoutMs,
|
|
63
|
+
logger,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export async function apply(ctx, config) {
|
|
67
|
+
let logger = {
|
|
68
|
+
debug: (m) => ctx.logger.debug(m),
|
|
69
|
+
info: (m) => ctx.logger.info(m),
|
|
70
|
+
warn: (m) => ctx.logger.warn(m),
|
|
71
|
+
error: (m) => ctx.logger.error(m),
|
|
72
|
+
};
|
|
73
|
+
const dataDir = resolveDataDir(config);
|
|
74
|
+
// dsh 宿主无持久化日志,镜像 info+ 到数据目录 memory.log 供蒸馏问题诊断
|
|
75
|
+
const fileLogger = withFileLog(dataDir, logger);
|
|
76
|
+
logger = fileLogger;
|
|
77
|
+
// 存储初始化失败只降级(禁用捕获/蒸馏),绝不拖垮宿主——
|
|
78
|
+
// 记忆是增强能力,数据目录不可写时 dsh 本体必须照常启动。
|
|
79
|
+
let storageOk = true;
|
|
80
|
+
try {
|
|
81
|
+
await ensureDir(dataDir);
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
storageOk = false;
|
|
85
|
+
logger.error(`[memory] 数据目录不可写,记忆功能停用: ${dataDir} (${err instanceof Error ? err.message : String(err)})`);
|
|
86
|
+
}
|
|
87
|
+
// ── 记忆模式运行时开关(官方 settings 服务,live 生效;缺失时恒开) ──
|
|
88
|
+
const live = registerLiveSettings(ctx, logger);
|
|
89
|
+
// ── 会话档位存储(sessionId → auto/chat/work/off;默认档 = config.family) ──
|
|
90
|
+
const modes = new SessionModeStore(dataDir, config.family, logger);
|
|
91
|
+
if (storageOk) {
|
|
92
|
+
try {
|
|
93
|
+
await modes.init();
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
logger.warn(`[memory] 会话档位载入失败(降级为默认档内存态): ${err instanceof Error ? err.message : String(err)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// ── 检索引擎(memory.db)与向量服务 ──
|
|
100
|
+
const embed = createEmbeddingService(config, logger);
|
|
101
|
+
const db = new MemoryDb(path.join(dataDir, 'memory.db'), embed.getDimensions(), logger);
|
|
102
|
+
// 插件卸载时关闭连接(WAL 落盘),注册一次即可
|
|
103
|
+
ctx.effect(() => () => db.close());
|
|
104
|
+
let dbInit = { needsReindex: false };
|
|
105
|
+
if (storageOk) {
|
|
106
|
+
try {
|
|
107
|
+
dbInit = db.init(embed.isReady() ? embed.getProviderInfo() : undefined);
|
|
108
|
+
if (db.isDegraded()) {
|
|
109
|
+
storageOk = false;
|
|
110
|
+
logger.error('[memory] 检索库初始化失败(schema/FTS 不可用),记忆功能停用');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
storageOk = false;
|
|
115
|
+
logger.error(`[memory] 检索库打开失败,记忆功能停用: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const stores = {
|
|
119
|
+
l0: new L0Store(dataDir, db, embed, logger),
|
|
120
|
+
l1: new L1Store(dataDir, db, embed, config.recall.strategy, logger),
|
|
121
|
+
// L2/L3 分族隔离:各自目录与文件(scenes/chat|work、persona-chat|work.md)
|
|
122
|
+
scenes: {
|
|
123
|
+
chat: new SceneStore(dataDir, 'chat', logger),
|
|
124
|
+
work: new SceneStore(dataDir, 'work', logger),
|
|
125
|
+
},
|
|
126
|
+
persona: {
|
|
127
|
+
chat: new PersonaStore(dataDir, 'chat', logger),
|
|
128
|
+
work: new PersonaStore(dataDir, 'work', logger),
|
|
129
|
+
},
|
|
130
|
+
state: new StateStore(StateStore.pathFor(dataDir)),
|
|
131
|
+
};
|
|
132
|
+
if (storageOk) {
|
|
133
|
+
try {
|
|
134
|
+
await Promise.all([
|
|
135
|
+
stores.l0.init(),
|
|
136
|
+
stores.l1.init(),
|
|
137
|
+
stores.scenes.chat.init(),
|
|
138
|
+
stores.scenes.work.init(),
|
|
139
|
+
stores.persona.chat.init(),
|
|
140
|
+
stores.persona.work.init(),
|
|
141
|
+
]);
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
storageOk = false;
|
|
145
|
+
logger.error(`[memory] 存储初始化失败,记忆功能停用: ${err instanceof Error ? err.message : String(err)}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// 检索能力与策略说明(能力位由降级结果决定)
|
|
149
|
+
const caps = db.getCapabilities();
|
|
150
|
+
if (storageOk && config.recall.strategy !== 'keyword' && !caps.vectorSearch) {
|
|
151
|
+
logger.warn(`[memory] 检索策略 ${config.recall.strategy} 需要向量能力(embedding 未启用/不可用),本次运行按 keyword(FTS5 BM25)检索`);
|
|
152
|
+
}
|
|
153
|
+
logger.info(`[memory] 数据目录: ${dataDir}(v${PLUGIN_VERSION},默认档=${config.family},检索: FTS=${caps.ftsSearch} 向量=${caps.vectorSearch}${storageOk ? '' : ',存储不可用已停用捕获/蒸馏'})`);
|
|
154
|
+
// 蒸馏模型路由:启动期解析一次并记录(路由错误是最难事后排查的问题之一)
|
|
155
|
+
try {
|
|
156
|
+
const route = await resolveModelRoute(ctx, config);
|
|
157
|
+
logger.info(`[memory] 蒸馏模型路由: ${route.provider}/${route.model}`);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
logger.warn(`[memory] 蒸馏模型路由解析失败: ${errDetail(err)}`);
|
|
161
|
+
}
|
|
162
|
+
// embedding 配置变化 → 后台全量重嵌入(不阻塞启动)。
|
|
163
|
+
// meta 只在重嵌入完全成功后写入(review P7):过早写入会让下次启动比对通过而跳过补齐。
|
|
164
|
+
if (storageOk && embed.isReady() && !db.isDegraded()) {
|
|
165
|
+
const info = embed.getProviderInfo();
|
|
166
|
+
if (dbInit.needsReindex) {
|
|
167
|
+
if (caps.vectorSearch) {
|
|
168
|
+
void (async () => {
|
|
169
|
+
try {
|
|
170
|
+
const r1 = await stores.l1.reindex();
|
|
171
|
+
const r0 = await stores.l0.reindex();
|
|
172
|
+
if (r1.failed === 0 && r0.failed === 0) {
|
|
173
|
+
db.markEmbeddingSynced(info);
|
|
174
|
+
logger.info(`[memory] 向量重建完成:L1 ${r1.written} 条,L0 ${r0.written} 条(原因:${dbInit.reason ?? '配置变化'})`);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
logger.warn(`[memory] 向量重建未完成(L1 失败 ${r1.failed},L0 失败 ${r0.failed}),下次启动/补齐周期重试`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
logger.warn(`[memory] 向量重建失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
// meta 已匹配或空库(initSchema 已写入),幂等重写一次
|
|
188
|
+
db.markEmbeddingSynced(info);
|
|
189
|
+
}
|
|
190
|
+
// 周期性向量补齐:嵌入失败的批次/漏网记录自动补上(30 分钟一次,缺量判定幂等)
|
|
191
|
+
if (caps.vectorSearch) {
|
|
192
|
+
const backfill = () => {
|
|
193
|
+
void (async () => {
|
|
194
|
+
try {
|
|
195
|
+
const l1Missing = db.countL1Vec() >= 0 && db.countL1Vec() < db.countL1();
|
|
196
|
+
const l0Missing = db.countL0Vec() >= 0 && db.countL0Vec() < db.countL0();
|
|
197
|
+
if (!l1Missing && !l0Missing)
|
|
198
|
+
return;
|
|
199
|
+
const r1 = await stores.l1.reindex();
|
|
200
|
+
const r0 = await stores.l0.reindex();
|
|
201
|
+
if (r1.failed === 0 && r0.failed === 0) {
|
|
202
|
+
db.markEmbeddingSynced(info);
|
|
203
|
+
logger.info(`[memory] 向量补齐完成:L1 ${r1.written} 条,L0 ${r0.written} 条`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
logger.warn(`[memory] 向量补齐失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
208
|
+
}
|
|
209
|
+
})();
|
|
210
|
+
};
|
|
211
|
+
ctx.effect(() => {
|
|
212
|
+
const timer = setInterval(backfill, 30 * 60_000);
|
|
213
|
+
const first = setTimeout(backfill, 60_000);
|
|
214
|
+
return () => {
|
|
215
|
+
clearInterval(timer);
|
|
216
|
+
clearTimeout(first);
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const runner = new MemoryRunner(ctx, config, stores, logger, live);
|
|
222
|
+
await runner.init();
|
|
223
|
+
if (storageOk) {
|
|
224
|
+
registerCapture(ctx, config, runner, stores.l0, logger, live, modes);
|
|
225
|
+
}
|
|
226
|
+
const recall = registerRecall(ctx, config, stores, logger, live, modes);
|
|
227
|
+
runner.setAfterRun(recall.invalidateProfile);
|
|
228
|
+
registerMemoryTools(ctx, config, stores, logger, modes);
|
|
229
|
+
registerMemoryRpc(ctx, config, stores, logger, {
|
|
230
|
+
degraded: () => !storageOk || db.isDegraded(),
|
|
231
|
+
pending: () => runner.pendingCount,
|
|
232
|
+
}, live, modes, dataDir);
|
|
233
|
+
logger.info(`[memory] L0~L3 分层蒸馏记忆插件就绪(L1 记忆 ${storageOk ? stores.l1.size : 0} 条 | 捕获=${storageOk && config.capture.enabled} | 蒸馏=${storageOk && config.extract.enabled} | 召回=${config.recall.enabled})`);
|
|
234
|
+
}
|
package/dist/llm.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM 桥:用 DSH 自身的 llm 服务跑蒸馏调用。
|
|
3
|
+
*/
|
|
4
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
5
|
+
import type { MemoryConfig } from './config.js';
|
|
6
|
+
import type { MemoryLogger } from './types.js';
|
|
7
|
+
export interface LlmCallOptions {
|
|
8
|
+
system: string;
|
|
9
|
+
user: string;
|
|
10
|
+
maxTokens?: number;
|
|
11
|
+
temperature?: number;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
/** 诊断日志(缺失时静默,不影响调用) */
|
|
14
|
+
logger?: MemoryLogger;
|
|
15
|
+
}
|
|
16
|
+
/** 解析蒸馏用的 provider/model:配置优先,其次当前默认选择。 */
|
|
17
|
+
export declare function resolveModelRoute(ctx: Context, cfg: MemoryConfig): Promise<{
|
|
18
|
+
provider: string;
|
|
19
|
+
model: string;
|
|
20
|
+
}>;
|
|
21
|
+
/**
|
|
22
|
+
* 一次完整蒸馏调用:流式收集文本,返回最终字符串。
|
|
23
|
+
* 失败(error/aborted finish)抛错,由调用方兜底。
|
|
24
|
+
*
|
|
25
|
+
* 文本只从 block-end(协议保证携带**组装完成的整块**)取;text-delta 仅在
|
|
26
|
+
* 适配器异常地没有发 block-end 时兜底。两者都累计会把输出翻倍。
|
|
27
|
+
*/
|
|
28
|
+
export declare function callLLM(ctx: Context, cfg: MemoryConfig, opts: LlmCallOptions): Promise<string>;
|
|
29
|
+
/** 带诊断日志的 parseJson:解析失败时记录原始输出摘录(模型输出异常排查的关键信息)。 */
|
|
30
|
+
export declare function parseJsonLogged<T>(raw: string, what: string, logger?: MemoryLogger): T;
|
|
31
|
+
/** 容错地解析 LLM 输出的 JSON(剥掉可能的 ```json 围栏)。 */
|
|
32
|
+
export declare function parseJson<T>(raw: string): T;
|