dsh-memory-tiers 0.5.0 → 0.8.1
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/ARCHITECTURE.md +2460 -1613
- package/README.md +268 -206
- package/client.mjs +608 -425
- package/compaction.mjs +50 -43
- package/dshllm.mjs +141 -0
- package/index.mjs +826 -662
- package/memory5.mjs +309 -39
- package/memory6.mjs +1553 -0
- package/package.json +63 -54
- package/reindex.mjs +162 -0
- package/settings.mjs +327 -241
- package/silicon.mjs +56 -8
package/compaction.mjs
CHANGED
|
@@ -54,6 +54,10 @@ function eventRole(ev) {
|
|
|
54
54
|
export async function compactSurface(session, mem, opts = {}) {
|
|
55
55
|
const keepTurns = Math.max(0, Number(opts.keepTurns ?? 1))
|
|
56
56
|
const archiveChars = Math.max(100, Number(opts.archiveChars ?? 1200))
|
|
57
|
+
// ★ 归档开关:autoCapture 已经把这一轮(问+答+工具)写进 L1 了,
|
|
58
|
+
// 这里再归档一次会把同一轮存两遍。所以调用方在"rows 已写成功"时传 archive:false,
|
|
59
|
+
// 只做"把历史换成占位",不再抄一份进记忆。
|
|
60
|
+
const doArchive = opts.archive !== false
|
|
57
61
|
const log = opts.log || console
|
|
58
62
|
|
|
59
63
|
const nodes = session?.surface?.nodes
|
|
@@ -107,52 +111,55 @@ export async function compactSurface(session, mem, opts = {}) {
|
|
|
107
111
|
const endSeq = sliceSeqs[sliceSeqs.length - 1]
|
|
108
112
|
|
|
109
113
|
// ── 先把要丢的内容归档进记忆,写成功才动手丢 ──
|
|
110
|
-
|
|
111
|
-
let
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
// archive:false 时整段跳过(这一轮已经由 autoCapture 的 rows 写进 L1 了)。
|
|
115
|
+
let archived = 0
|
|
116
|
+
if (doArchive) {
|
|
117
|
+
const lines = []
|
|
118
|
+
let totalChars = 0
|
|
119
|
+
let dropped = 0
|
|
120
|
+
// ★ 只归档「真人在说」「模型在答」「工具结果」三类。
|
|
121
|
+
// surface 里还混着 runtime-context(工作目录/沙箱策略)、skill-catalog(技能清单)、
|
|
122
|
+
// time-context(时间快照)——这些是框架注入的,归档进去不仅没用,
|
|
123
|
+
// 还会把注入块(含记忆块自身)原样抄进记忆,形成自我污染。
|
|
124
|
+
const ARCHIVABLE = new Set(['user', 'model', 'tool'])
|
|
125
|
+
for (const seq of sliceSeqs) {
|
|
126
|
+
const ev = session.eventAt(seq)
|
|
127
|
+
if (!ev) continue
|
|
128
|
+
const src = (ev.data?.message ?? ev.data)?.source
|
|
129
|
+
if (!ARCHIVABLE.has(src?.kind)) { dropped++; continue }
|
|
130
|
+
const t = eventText(ev)
|
|
131
|
+
if (!t) { dropped++; continue }
|
|
132
|
+
const line = `${eventRole(ev)}: ${t}`
|
|
133
|
+
if (totalChars + line.length > archiveChars) {
|
|
134
|
+
// ★ 不能整段丢掉:至少保留能塞下的部分,否则归档变成一句「(还有 N 条略)」,
|
|
135
|
+
// 这个 bug 在第一次上线时真实发生过(3 条空归档躺在 L1 里)。
|
|
136
|
+
const room = archiveChars - totalChars
|
|
137
|
+
if (room > 60) {
|
|
138
|
+
lines.push(line.slice(0, room - 1) + '…')
|
|
139
|
+
totalChars += room
|
|
140
|
+
}
|
|
141
|
+
dropped += sliceSeqs.length - lines.length
|
|
142
|
+
break
|
|
133
143
|
}
|
|
134
|
-
|
|
135
|
-
|
|
144
|
+
lines.push(line)
|
|
145
|
+
totalChars += line.length
|
|
136
146
|
}
|
|
137
|
-
lines.push(
|
|
138
|
-
totalChars += line.length
|
|
139
|
-
}
|
|
140
|
-
if (dropped > 0 && lines.length) lines.push(`…(另有 ${dropped} 条系统注入或过长内容未收录)`)
|
|
147
|
+
if (dropped > 0 && lines.length) lines.push(`…(另有 ${dropped} 条系统注入或过长内容未收录)`)
|
|
141
148
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
if (lines.length && mem?.writeMany) {
|
|
150
|
+
try {
|
|
151
|
+
const rounds = userIdx.filter((i) => i < keepStart).length
|
|
152
|
+
const res = await mem.writeMany([{
|
|
153
|
+
text: `[归档] 前 ${rounds} 轮对话(已被压缩出上下文):\n${lines.join('\n')}`,
|
|
154
|
+
speaker: 'archive',
|
|
155
|
+
channel: 'dialog',
|
|
156
|
+
}])
|
|
157
|
+
archived = Array.isArray(res) ? res.length : 1
|
|
158
|
+
} catch (err) {
|
|
159
|
+
// ★ 归档失败就绝不压缩:宁可上下文大,也不能丢内容
|
|
160
|
+
log.warn?.('[memory-tiers] 压缩前归档失败,放弃本次压缩:%s', err?.message ?? err)
|
|
161
|
+
return { compacted: false, removed: 0, archived: 0, reason: 'archive-failed' }
|
|
162
|
+
}
|
|
156
163
|
}
|
|
157
164
|
}
|
|
158
165
|
|
package/dshllm.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 走 dsh 自己的会话模型(不需要额外的 key)
|
|
3
|
+
*
|
|
4
|
+
* 背景:融合判关系、摘要这类"小活",用户希望直接跟着当前对话用的模型走——
|
|
5
|
+
* 对话用 flash 它就 flash,用 pro 它就 pro,不用再单独配一家 provider。
|
|
6
|
+
*
|
|
7
|
+
* dsh 有两个服务能给到这件事:
|
|
8
|
+
* ctx.get('llm') → prepareCall({provider, model}, signal, configure)
|
|
9
|
+
* 官方插件 dsh-session-title-llm 就是这么打辅助请求的
|
|
10
|
+
* ctx.get('agentDefaultModel') → currentSelection() 返回 {provider, model, reasoningEffort}
|
|
11
|
+
*
|
|
12
|
+
* 流式返回,chunk 形状(照 dsh-llm 的 BlockAssembler):
|
|
13
|
+
* {type:'block-start'} {type:'text-delta', text} {type:'reasoning-delta', text}
|
|
14
|
+
* {type:'block-end'} {type:'usage', usage} {type:'finish', reason}
|
|
15
|
+
* 我们只收 text-delta,reasoning-delta 一律丢掉(那就是思考过程)。
|
|
16
|
+
*
|
|
17
|
+
* 关思考:没有统一的 enable_thinking 开关。做法是从 modelInfo.reasoning.efforts
|
|
18
|
+
* 里取**最低档**(efforts 是从小到大排的),等价于这个模型能选的"最少思考"。
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const DEFAULT_TIMEOUT_MS = 30000
|
|
22
|
+
|
|
23
|
+
export function createSessionCaller(getCtx, opts = {}) {
|
|
24
|
+
const log = opts.log
|
|
25
|
+
let cached
|
|
26
|
+
let cacheTried = false
|
|
27
|
+
|
|
28
|
+
/** 拿到 llm / agentDefaultModel 两个服务;拿不到就记下来不再反复试 */
|
|
29
|
+
function services() {
|
|
30
|
+
if (cached || cacheTried) return cached || null
|
|
31
|
+
cacheTried = true
|
|
32
|
+
try {
|
|
33
|
+
const ctx = typeof getCtx === 'function' ? getCtx() : getCtx
|
|
34
|
+
if (!ctx) return null
|
|
35
|
+
const llm = (typeof ctx.get === 'function' ? ctx.get('llm') : null) ?? ctx.llm
|
|
36
|
+
const dm = (typeof ctx.get === 'function' ? ctx.get('agentDefaultModel') : null) ?? ctx.agentDefaultModel
|
|
37
|
+
if (!llm || typeof llm.prepareCall !== 'function') return null
|
|
38
|
+
cached = { llm, dm }
|
|
39
|
+
} catch (err) {
|
|
40
|
+
log?.warn?.('[memory-tiers] 取 dsh llm 服务失败:%s', err?.message ?? err)
|
|
41
|
+
}
|
|
42
|
+
return cached || null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 当前会话模型:{provider, model, reasoningEffort} 或 null */
|
|
46
|
+
function selection() {
|
|
47
|
+
const s = services()
|
|
48
|
+
if (!s?.dm || typeof s.dm.currentSelection !== 'function') return null
|
|
49
|
+
try {
|
|
50
|
+
const sel = s.dm.currentSelection()
|
|
51
|
+
if (!sel?.provider || !sel?.model) return null
|
|
52
|
+
return sel
|
|
53
|
+
} catch { return null }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function available() {
|
|
57
|
+
return !!selection()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** 关思考用:取模型支持的最低 reasoningEffort */
|
|
61
|
+
function lowestEffort(modelInfo) {
|
|
62
|
+
const efforts = modelInfo?.reasoning?.efforts
|
|
63
|
+
if (Array.isArray(efforts) && efforts.length && efforts[0]?.id) return efforts[0].id
|
|
64
|
+
return undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 打一次请求
|
|
69
|
+
* @returns {string} 模型输出的纯文本(已 trim);失败抛错,由调用方回落
|
|
70
|
+
*/
|
|
71
|
+
async function call({
|
|
72
|
+
system = '',
|
|
73
|
+
user = '',
|
|
74
|
+
maxTokens = 400,
|
|
75
|
+
temperature = 0,
|
|
76
|
+
disableThinking = true,
|
|
77
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
78
|
+
}) {
|
|
79
|
+
const s = services()
|
|
80
|
+
if (!s) throw new Error('拿不到 dsh 的 llm 服务')
|
|
81
|
+
const sel = selection()
|
|
82
|
+
if (!sel) throw new Error('取不到当前会话模型(agentDefaultModel 未就绪)')
|
|
83
|
+
|
|
84
|
+
const signal = AbortSignal.timeout(timeoutMs)
|
|
85
|
+
|
|
86
|
+
// prepareCall 的第三个参数是 configure(controls, modelInfo):
|
|
87
|
+
// 只能改 controls(推理强度/温度/上限),不能改路由(provider/model)
|
|
88
|
+
const call = await s.llm.prepareCall(
|
|
89
|
+
{ provider: sel.provider, model: sel.model, maxTokens },
|
|
90
|
+
signal,
|
|
91
|
+
(controls, modelInfo) => {
|
|
92
|
+
const next = { ...controls }
|
|
93
|
+
if (temperature !== undefined) next.temperature = temperature
|
|
94
|
+
if (disableThinking) {
|
|
95
|
+
const effort = lowestEffort(modelInfo)
|
|
96
|
+
if (effort !== undefined) next.reasoningEffort = effort
|
|
97
|
+
}
|
|
98
|
+
return next
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
// ★ 不能再改 maxTokens / temperature:prepareCall 已经把配置冻住了,
|
|
103
|
+
// 事后改会触发 "prepared LLM call config changed before adapter dispatch"。
|
|
104
|
+
const options = {
|
|
105
|
+
...call.config,
|
|
106
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: String(user) }] }],
|
|
107
|
+
system: String(system || ''),
|
|
108
|
+
purpose: 'memory-tiers',
|
|
109
|
+
signal,
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
let text = ''
|
|
113
|
+
let finish = null
|
|
114
|
+
for await (const chunk of call.stream(options)) {
|
|
115
|
+
const t = chunk?.type
|
|
116
|
+
if (t === 'text-delta') text += chunk.text ?? ''
|
|
117
|
+
else if (t === 'finish') { finish = chunk.reason; break }
|
|
118
|
+
else if (t === 'error') finish = { kind: 'error', failure: { message: chunk?.message ?? '流式返回错误' } }
|
|
119
|
+
else if (t === 'usage') { /* 不记账:会话模型的用量算在对话账上 */ }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (finish?.kind === 'error' || finish?.kind === 'aborted') {
|
|
123
|
+
throw new Error(String(finish?.failure?.message ?? finish?.kind ?? '会话模型调用失败'))
|
|
124
|
+
}
|
|
125
|
+
return { text: text.trim(), provider: sel.provider, model: sel.model }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** 给设置页看的:能不能用、当前是哪个模型、支持的最低思考档 */
|
|
129
|
+
function info() {
|
|
130
|
+
const sel = selection()
|
|
131
|
+
if (!sel) return { available: false, reason: services() ? 'agentDefaultModel 未就绪' : '未取到 llm 服务' }
|
|
132
|
+
return {
|
|
133
|
+
available: true,
|
|
134
|
+
provider: sel.provider,
|
|
135
|
+
model: sel.model,
|
|
136
|
+
reasoningEffort: sel.reasoningEffort ?? null,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { available, selection, call, info }
|
|
141
|
+
}
|