dsh-layered-memory 0.8.3 → 0.8.5
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.en.md +38 -17
- package/README.md +34 -17
- package/assets/img/ui-dark.jpg +0 -0
- package/assets/img/ui-light.jpg +0 -0
- package/assets/readme/bench-dialog.svg +53 -70
- package/assets/readme/bench-workflow.svg +29 -29
- package/dist/bench-control.d.ts +35 -0
- package/dist/bench-control.js +16 -0
- package/dist/client.js +205 -29
- package/dist/config.d.ts +12 -0
- package/dist/config.js +9 -1
- package/dist/hooks/recall.d.ts +23 -0
- package/dist/hooks/recall.js +36 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +16 -1
- package/dist/llm-usage.d.ts +27 -0
- package/dist/llm-usage.js +39 -0
- package/dist/llm.d.ts +11 -2
- package/dist/llm.js +20 -6
- package/dist/pipeline/l1.js +4 -2
- package/dist/pipeline/l2.js +1 -0
- package/dist/pipeline/l3.js +1 -0
- package/dist/pipeline/runner.d.ts +16 -0
- package/dist/pipeline/runner.js +60 -0
- package/dist/prompts/l1-extraction.d.ts +7 -1
- package/dist/prompts/l1-extraction.js +12 -3
- package/dist/settings.d.ts +3 -3
- package/dist/settings.js +3 -3
- package/dist/stats.d.ts +27 -1
- package/dist/stats.js +42 -5
- package/dist/store/l0.d.ts +2 -0
- package/dist/store/l0.js +4 -0
- package/dist/store/sqlite.d.ts +2 -0
- package/dist/store/sqlite.js +14 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +8 -0
- package/package.json +17 -17
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bench 控制服务(config `benchControl` 门控,默认关):为同进程的基准驱动插件
|
|
3
|
+
* (dsh-bench-runner 的 lifecycle 赛道)提供进程内控制面。
|
|
4
|
+
*
|
|
5
|
+
* 为什么不走 RPC:宿主侧 connection.rpc 只有 handle/intercept、没有 call(),
|
|
6
|
+
* 基准驱动包在 dsh 宿主进程内无法调用插件的 loopback RPC 端点;cordis 服务
|
|
7
|
+
* (ctx.provide / ctx.get)是唯一干净的进程内通道。生产部署不开启该配置,
|
|
8
|
+
* 服务不注册、零表面积;即便开启,暴露的也只是既有公开 API 的薄包装,
|
|
9
|
+
* 不引入新逻辑:
|
|
10
|
+
* - RebuildController.start()/getStatus()(重建触发与状态轮询);
|
|
11
|
+
* - SessionModeStore.set()/get()(会话档位——capture/recall 每轮读 Map,
|
|
12
|
+
* agent 建好后、首条消息前设档即可生效,且走 onModeChange 回调的
|
|
13
|
+
* pending 落袋/挂起语义,不是裸改 Map)。
|
|
14
|
+
*/
|
|
15
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
16
|
+
import type { MemoryLogger, MemoryMode } from './types.js';
|
|
17
|
+
import type { RebuildController, RebuildStatus } from './pipeline/rebuild.js';
|
|
18
|
+
import type { SessionModeStore } from './store/session-modes.js';
|
|
19
|
+
import type { DistillUsageSnapshot } from './llm-usage.js';
|
|
20
|
+
/** 服务名(消费方:bench/harness/dsh-bench-runner 的 lifecycle 赛道)。 */
|
|
21
|
+
export declare const BENCH_CONTROL_SERVICE = "dsh-memory-bench";
|
|
22
|
+
export interface BenchControlSurface {
|
|
23
|
+
/** 触发全量重建(从 L0 重导派生层);前置条件不满足时抛错(调用方捕获)。 */
|
|
24
|
+
rebuildStart(): RebuildStatus;
|
|
25
|
+
/** 重建状态快照(phase/running/recordsBuilt/…,轮询至 done/failed/cancelled)。 */
|
|
26
|
+
rebuildStatus(): RebuildStatus;
|
|
27
|
+
/** 设置会话档位(chat/work/off/auto);对全新会话应在首条消息前设置。 */
|
|
28
|
+
setSessionMode(sessionId: string, mode: MemoryMode): void;
|
|
29
|
+
/** 查询会话档位(未设过的会话返回部署默认档)。 */
|
|
30
|
+
getSessionMode(sessionId: string): MemoryMode;
|
|
31
|
+
/** 蒸馏用量快照(按层累计的调用数/输入字符/输出 token——「记忆开销」记账)。 */
|
|
32
|
+
getDistillUsage(): DistillUsageSnapshot;
|
|
33
|
+
}
|
|
34
|
+
/** 注册控制服务,返回注销函数(调用方在插件 dispose 时执行)。 */
|
|
35
|
+
export declare function registerBenchControl(ctx: Context, rebuild: RebuildController, modes: SessionModeStore, logger: MemoryLogger): () => void;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { snapshotDistillUsage } from './llm-usage.js';
|
|
2
|
+
/** 服务名(消费方:bench/harness/dsh-bench-runner 的 lifecycle 赛道)。 */
|
|
3
|
+
export const BENCH_CONTROL_SERVICE = 'dsh-memory-bench';
|
|
4
|
+
/** 注册控制服务,返回注销函数(调用方在插件 dispose 时执行)。 */
|
|
5
|
+
export function registerBenchControl(ctx, rebuild, modes, logger) {
|
|
6
|
+
const surface = {
|
|
7
|
+
rebuildStart: () => rebuild.start(),
|
|
8
|
+
rebuildStatus: () => rebuild.getStatus(),
|
|
9
|
+
setSessionMode: (sessionId, mode) => modes.set(sessionId, mode),
|
|
10
|
+
getSessionMode: (sessionId) => modes.get(sessionId),
|
|
11
|
+
getDistillUsage: () => snapshotDistillUsage(),
|
|
12
|
+
};
|
|
13
|
+
const dispose = ctx.provide(BENCH_CONTROL_SERVICE, surface);
|
|
14
|
+
logger.info('[memory] bench 控制服务已提供(dsh-memory-bench,仅基准/调试部署)');
|
|
15
|
+
return dispose;
|
|
16
|
+
}
|
package/dist/client.js
CHANGED
|
@@ -244,6 +244,23 @@ window.__ModuleLoader__.load({
|
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/** 相对时间(悬浮卡摘要行用):刚刚 / N 分钟前 / N 小时前 / N 天前;无效返回 null。 */
|
|
248
|
+
function fmtAgo(iso) {
|
|
249
|
+
if (!iso) return null;
|
|
250
|
+
try {
|
|
251
|
+
var t = new Date(iso).getTime();
|
|
252
|
+
if (!t) return null;
|
|
253
|
+
var s = Math.floor((Date.now() - t) / 1000);
|
|
254
|
+
if (s < 0) s = 0;
|
|
255
|
+
if (s < 45) return "刚刚";
|
|
256
|
+
if (s < 3600) return Math.floor(s / 60) + " 分钟前";
|
|
257
|
+
if (s < 86400) return Math.floor(s / 3600) + " 小时前";
|
|
258
|
+
return Math.floor(s / 86400) + " 天前";
|
|
259
|
+
} catch (e) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
247
264
|
// ── Switch 组件 ──
|
|
248
265
|
function Switch(props) {
|
|
249
266
|
var on = !!props.checked;
|
|
@@ -673,6 +690,151 @@ window.__ModuleLoader__.load({
|
|
|
673
690
|
props.error
|
|
674
691
|
? react.createElement("div", { style: { fontSize: 11, color: "var(--dsh-mem-danger)", marginTop: 10, whiteSpace: "nowrap" } }, props.error)
|
|
675
692
|
: null,
|
|
693
|
+
// 会话信息区(分隔线 + 2×2 指标 + 状态行):session-stats 热路径端点,
|
|
694
|
+
// 宿主不支持 / 数据缺失时整体不渲染(best-effort 增强,不占位)
|
|
695
|
+
props.rpc && props.sessionId
|
|
696
|
+
? react.createElement(SessionInfoArea, { rpc: props.rpc, sessionId: props.sessionId })
|
|
697
|
+
: null,
|
|
698
|
+
),
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// ── 会话信息区:悬浮卡下半部(召回命中 / 攒批进度 / 会话产出 / 会话消息) ──
|
|
703
|
+
// 数据通道 dsh-memory/session-stats(宿主侧零文件 I/O:内存注册表 + 索引 COUNT,
|
|
704
|
+
// 见 slider-spec 数据策略节);自适应轮询:忙时(攒批/挂起/全局待蒸馏)2s,静默 5s,
|
|
705
|
+
// 浮层卸载即停(ModeSlider 只在 pill 展开期间挂载)。
|
|
706
|
+
function sinfoCell(val, label, title) {
|
|
707
|
+
return react.createElement(
|
|
708
|
+
"div",
|
|
709
|
+
{ title: title || null },
|
|
710
|
+
react.createElement("div", { className: "dsh-mem-sinfo-val" }, val),
|
|
711
|
+
react.createElement("div", { className: "dsh-mem-sinfo-label" }, label),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function SessionInfoArea(props) {
|
|
716
|
+
var rpc = props.rpc;
|
|
717
|
+
var sessionId = props.sessionId;
|
|
718
|
+
// undefined=首帧加载中;null=宿主不支持(整体隐藏);对象=最新快照
|
|
719
|
+
var stState = react.useState(undefined);
|
|
720
|
+
var stats = stState[0];
|
|
721
|
+
var setStats = stState[1];
|
|
722
|
+
var busyRef = react.useRef(false);
|
|
723
|
+
|
|
724
|
+
react.useEffect(function () {
|
|
725
|
+
if (!rpc || !sessionId) return undefined;
|
|
726
|
+
var alive = true;
|
|
727
|
+
var timer = null;
|
|
728
|
+
var seq = 0;
|
|
729
|
+
var tick = function () {
|
|
730
|
+
var token = ++seq;
|
|
731
|
+
rpc("dsh-memory/session-stats", { sessionId: sessionId })
|
|
732
|
+
.then(function (r) {
|
|
733
|
+
if (!alive || token !== seq) return;
|
|
734
|
+
if (r && r.ok && r.value && r.value.supported === false) {
|
|
735
|
+
setStats(null); // 宿主无数据源:整体隐藏
|
|
736
|
+
} else if (r && r.ok && r.value) {
|
|
737
|
+
setStats(r.value);
|
|
738
|
+
var d = r.value.distill || {};
|
|
739
|
+
var g = r.value.global || {};
|
|
740
|
+
busyRef.current = (d.pendingSlice || 0) > 0 || (d.parkedSlices || 0) > 0 || (g.pendingTotal || 0) > 0;
|
|
741
|
+
}
|
|
742
|
+
// RPC 失败:保持旧快照(信息区不因瞬时错误闪没)
|
|
743
|
+
})
|
|
744
|
+
.catch(function () {})
|
|
745
|
+
.then(function () {
|
|
746
|
+
if (alive) timer = setTimeout(tick, busyRef.current ? 2000 : 5000);
|
|
747
|
+
});
|
|
748
|
+
};
|
|
749
|
+
tick();
|
|
750
|
+
return function () {
|
|
751
|
+
alive = false;
|
|
752
|
+
if (timer) clearTimeout(timer);
|
|
753
|
+
};
|
|
754
|
+
}, [rpc, sessionId]);
|
|
755
|
+
|
|
756
|
+
// 首帧占位骨架(防内容跳变);宿主不支持则整体隐藏
|
|
757
|
+
if (stats === null) return null;
|
|
758
|
+
if (stats === undefined) {
|
|
759
|
+
return react.createElement(
|
|
760
|
+
"div",
|
|
761
|
+
{ className: "dsh-mem-sinfo" },
|
|
762
|
+
react.createElement(
|
|
763
|
+
"div",
|
|
764
|
+
{ className: "dsh-mem-sinfo-grid" },
|
|
765
|
+
sinfoCell("…", "召回命中"),
|
|
766
|
+
sinfoCell("…", "攒批进度"),
|
|
767
|
+
sinfoCell("…", "本会话记忆"),
|
|
768
|
+
sinfoCell("…", "会话消息"),
|
|
769
|
+
),
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
var rc = stats.recall || {};
|
|
774
|
+
var di = stats.distill || {};
|
|
775
|
+
var gl = stats.global || {};
|
|
776
|
+
var isOff = stats.mode === "off";
|
|
777
|
+
|
|
778
|
+
// 召回命中:口径是"注入统计"(命中轮次/检索轮次),停用时显示状态而非误导性 0/0
|
|
779
|
+
var rcVal;
|
|
780
|
+
var rcLabel;
|
|
781
|
+
var rcTitle;
|
|
782
|
+
if (rc.enabled === false) {
|
|
783
|
+
rcVal = "停用";
|
|
784
|
+
rcLabel = "召回命中";
|
|
785
|
+
rcTitle = "召回已停用(开关关闭 / 档位关闭 / 部署未启用)";
|
|
786
|
+
} else {
|
|
787
|
+
rcVal = (rc.hitTurns || 0) + "/" + (rc.injectedTurns || 0);
|
|
788
|
+
rcLabel = "召回命中 · " + (rc.totalHits || 0) + " 条";
|
|
789
|
+
rcTitle =
|
|
790
|
+
"最近一轮命中 " + (rc.lastHits || 0) + " 条,耗时 " + (rc.lastDurationMs || 0) + "ms" +
|
|
791
|
+
((rc.timeouts || 0) > 0 ? ",超时跳过 " + rc.timeouts + " 次" : "");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// 攒批进度(x/生效阈值,含 warmup 爬坡);off 档显示挂起切片数
|
|
795
|
+
var dVal;
|
|
796
|
+
var dLabel;
|
|
797
|
+
var dTitle;
|
|
798
|
+
if (isOff) {
|
|
799
|
+
dVal = String(di.parkedSlices || 0);
|
|
800
|
+
dLabel = "挂起切片";
|
|
801
|
+
dTitle = "档位关闭:未蒸馏切片挂起,切回档位后继续";
|
|
802
|
+
} else {
|
|
803
|
+
dVal = (di.pendingSlice || 0) + "/" + (di.threshold != null ? di.threshold : "-");
|
|
804
|
+
dLabel = (di.parkedSlices || 0) > 0 ? "攒批 · 挂起 " + di.parkedSlices : "攒批进度";
|
|
805
|
+
dTitle = "达到阈值后自动蒸馏(阈值随使用渐进爬坡到稳态)";
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
var pTitle = di.lastDistillAt ? "最近蒸馏 " + fmtTime(di.lastDistillAt) : "本会话尚未蒸馏";
|
|
809
|
+
var warn = gl.degraded ? "⚠ 存储不可用,记忆功能已停用" : null;
|
|
810
|
+
var note = null;
|
|
811
|
+
if (!gl.degraded) {
|
|
812
|
+
if (stats.retrieval === "keyword" && !isOff) note = "检索降级:纯关键词(向量不可用)";
|
|
813
|
+
else if (stats.retrieval === "none") note = "检索不可用(FTS 与向量均失效)";
|
|
814
|
+
}
|
|
815
|
+
var ago = fmtAgo(gl.lastExtractAt);
|
|
816
|
+
|
|
817
|
+
return react.createElement(
|
|
818
|
+
"div",
|
|
819
|
+
{ className: "dsh-mem-sinfo" },
|
|
820
|
+
warn
|
|
821
|
+
? react.createElement("div", { className: "dsh-mem-sinfo-warn" }, warn)
|
|
822
|
+
: null,
|
|
823
|
+
react.createElement(
|
|
824
|
+
"div",
|
|
825
|
+
{ className: "dsh-mem-sinfo-grid" },
|
|
826
|
+
sinfoCell(rcVal, rcLabel, rcTitle),
|
|
827
|
+
sinfoCell(dVal, dLabel, dTitle),
|
|
828
|
+
sinfoCell(String(di.producedRecords || 0), "本会话记忆", pTitle),
|
|
829
|
+
sinfoCell(stats.l0Count != null ? String(stats.l0Count) : "…", "会话消息"),
|
|
830
|
+
),
|
|
831
|
+
note
|
|
832
|
+
? react.createElement("div", { className: "dsh-mem-sinfo-note" }, note)
|
|
833
|
+
: null,
|
|
834
|
+
react.createElement(
|
|
835
|
+
"div",
|
|
836
|
+
{ className: "dsh-mem-sinfo-sum" },
|
|
837
|
+
"待蒸馏 " + (gl.pendingTotal || 0) + " · 上次蒸馏 " + (ago || "尚未蒸馏"),
|
|
676
838
|
),
|
|
677
839
|
);
|
|
678
840
|
}
|
|
@@ -930,6 +1092,15 @@ window.__ModuleLoader__.load({
|
|
|
930
1092
|
" box-shadow: 0 16px 48px rgba(0,0,0,0.24); }",
|
|
931
1093
|
"body[data-ds-dark-theme] .dsh-mem-rb-modal { box-shadow: 0 16px 48px rgba(0,0,0,0.6); }",
|
|
932
1094
|
".dsh-mem-rb-muted { font-size: 12px; color: var(--dsh-mem-text-3); }",
|
|
1095
|
+
// ── 会话信息区(悬浮卡下半部):分隔线 + 2×2 指标 + 状态行;纯静态 DOM,
|
|
1096
|
+
// 不进粒子层 rAF 循环,轮询数据到达才触发本组件小树 re-render ──
|
|
1097
|
+
".dsh-mem-sinfo { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--dsh-mem-border); }",
|
|
1098
|
+
".dsh-mem-sinfo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; }",
|
|
1099
|
+
".dsh-mem-sinfo-val { font-size: 13px; font-weight: 600; color: var(--dsh-mem-text-1); line-height: 18px; font-variant-numeric: tabular-nums; }",
|
|
1100
|
+
".dsh-mem-sinfo-label { font-size: 11px; color: var(--dsh-mem-text-3); line-height: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }",
|
|
1101
|
+
".dsh-mem-sinfo-warn { font-size: 11px; color: var(--dsh-mem-danger); line-height: 16px; margin-bottom: 6px; }",
|
|
1102
|
+
".dsh-mem-sinfo-note { font-size: 11px; color: var(--dsh-mem-text-3); line-height: 16px; margin-top: 8px; }",
|
|
1103
|
+
".dsh-mem-sinfo-sum { font-size: 11px; color: var(--dsh-mem-text-3); line-height: 16px; margin-top: 8px; }",
|
|
933
1104
|
// reduced-motion 兜底放样式表末尾:同特异性下后置声明才能压过上面的组件类
|
|
934
1105
|
"@media (prefers-reduced-motion: reduce) {",
|
|
935
1106
|
" .dsh-mem-root, .dsh-mem-root *, .dsh-mem-btn, .dsh-mem-input, .dsh-mem-select, .dsh-mem-rb-fill, .dsh-mem-scene-chev, .dsh-mem-sel-chev { transition: none; }",
|
|
@@ -1066,6 +1237,8 @@ window.__ModuleLoader__.load({
|
|
|
1066
1237
|
mode: mode || "auto",
|
|
1067
1238
|
onCommit: commit,
|
|
1068
1239
|
error: error,
|
|
1240
|
+
rpc: rpc,
|
|
1241
|
+
sessionId: sessionId,
|
|
1069
1242
|
})
|
|
1070
1243
|
: null,
|
|
1071
1244
|
);
|
|
@@ -2044,7 +2217,7 @@ window.__ModuleLoader__.load({
|
|
|
2044
2217
|
"div", { style: S.switchDesc },
|
|
2045
2218
|
"各蒸馏层单次输出的 token 上限(抽取/去重/L2/L3);留空或 0 = 跟随默认(当前生效 " +
|
|
2046
2219
|
layers.map(function (l) { return eff[l[0]] || "?"; }).join(" / ") +
|
|
2047
|
-
");思考档 high/max 时实际限额自动 ×4",
|
|
2220
|
+
");思考档 high/xhigh/max 时实际限额自动 ×4",
|
|
2048
2221
|
)),
|
|
2049
2222
|
),
|
|
2050
2223
|
react.createElement(
|
|
@@ -2197,34 +2370,37 @@ window.__ModuleLoader__.load({
|
|
|
2197
2370
|
// 分组:蒸馏参数(模型路由 / 思考档位 / 输出预算)
|
|
2198
2371
|
react.createElement("div", { style: S.panelLabel }, "蒸馏参数"),
|
|
2199
2372
|
settingsData.effort
|
|
2200
|
-
?
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
react.createElement(
|
|
2222
|
-
"div",
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2373
|
+
? (function () {
|
|
2374
|
+
// 档位表来自当前生效模型的能力声明(settings-get 的 effort.options;
|
|
2375
|
+
// 空声明 → 只显示 high)。首项固定「自动」(key='',点击回写空串=
|
|
2376
|
+
// 按模型能力解析),选过显式档位后仍可回到自动;失效档位高亮实际生效值。
|
|
2377
|
+
var f = settingsData.effort;
|
|
2378
|
+
var opts = f.options && f.options.length ? f.options : ["high"];
|
|
2379
|
+
var segOptions = [{ key: "", label: "自动" }].concat(
|
|
2380
|
+
opts.map(function (k) { return { key: k, label: k }; })
|
|
2381
|
+
);
|
|
2382
|
+
var segValue = f.current === "" || opts.indexOf(f.current) >= 0
|
|
2383
|
+
? f.current
|
|
2384
|
+
: opts.indexOf(f.effective) >= 0 ? f.effective : "high";
|
|
2385
|
+
return react.createElement(
|
|
2386
|
+
"div",
|
|
2387
|
+
{ style: S.switchRow },
|
|
2388
|
+
react.createElement(Segmented, {
|
|
2389
|
+
value: segValue,
|
|
2390
|
+
options: segOptions,
|
|
2391
|
+
disabled: !master,
|
|
2392
|
+
onChange: function (v) { toggle("reasoningEffort", v); },
|
|
2393
|
+
}),
|
|
2394
|
+
react.createElement("div", null,
|
|
2395
|
+
react.createElement("div", { style: S.switchLabel }, "蒸馏思考"),
|
|
2396
|
+
react.createElement(
|
|
2397
|
+
"div",
|
|
2398
|
+
{ style: S.switchDesc },
|
|
2399
|
+
"当前生效 " + (f.effective || "(不传,跟随模型默认)") +
|
|
2400
|
+
(f.current ? "" : "(自动)"),
|
|
2401
|
+
)),
|
|
2402
|
+
);
|
|
2403
|
+
})()
|
|
2228
2404
|
: null,
|
|
2229
2405
|
react.createElement(LlmModelRow, { rpc: rpc, disabled: !master }),
|
|
2230
2406
|
react.createElement(BudgetInputs, {
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import Schema from '@deepseek-ai/schemastery';
|
|
7
7
|
import type { ExtractMode } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* 蒸馏思考档位全词汇表(唯一事实源):'' = 自动(模型默认档 → high),
|
|
10
|
+
* 其余为各适配器通用档位词汇表(deepseek 认 'off',OpenAI 系是 'none')。
|
|
11
|
+
* schema(config/settings)、运行时解析(settings.resolveSettings)与 RPC
|
|
12
|
+
* 写入门(stats.settings-set)共用,勿在别处再抄字面量表。
|
|
13
|
+
*/
|
|
14
|
+
export declare const EFFORT_CHOICES: readonly ["", "off", "none", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
8
15
|
export interface MemoryConfig {
|
|
9
16
|
/** 数据目录;留空则用 $DSH_HOME/memory。 */
|
|
10
17
|
dataDir: string;
|
|
@@ -106,6 +113,9 @@ export interface MemoryConfig {
|
|
|
106
113
|
};
|
|
107
114
|
/** 是否注册模型可调用的记忆工具。 */
|
|
108
115
|
tools: boolean;
|
|
116
|
+
/** 注册 bench 控制服务(dsh-memory-bench,进程内 rebuild 触发面)。
|
|
117
|
+
* 仅供基准/调试部署(bench profile 的 lifecycle 赛道),默认关——生产零表面积。 */
|
|
118
|
+
benchControl: boolean;
|
|
109
119
|
}
|
|
110
120
|
export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
111
121
|
dataDir: Schema<string, string>;
|
|
@@ -212,6 +222,7 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
212
222
|
timeoutMs: Schema<number, number>;
|
|
213
223
|
}>>;
|
|
214
224
|
tools: Schema<boolean, boolean>;
|
|
225
|
+
benchControl: Schema<boolean, boolean>;
|
|
215
226
|
}>, Schemastery.ObjectT<{
|
|
216
227
|
dataDir: Schema<string, string>;
|
|
217
228
|
family: Schema<"chat" | "work" | "auto", "chat" | "work" | "auto">;
|
|
@@ -317,5 +328,6 @@ export declare const memorySchema: Schema<Schemastery.ObjectS<{
|
|
|
317
328
|
timeoutMs: Schema<number, number>;
|
|
318
329
|
}>>;
|
|
319
330
|
tools: Schema<boolean, boolean>;
|
|
331
|
+
benchControl: Schema<boolean, boolean>;
|
|
320
332
|
}>>;
|
|
321
333
|
export declare function resolveDataDir(cfg: MemoryConfig): string;
|
package/dist/config.js
CHANGED
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import Schema from '@deepseek-ai/schemastery';
|
|
7
7
|
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
8
|
+
/**
|
|
9
|
+
* 蒸馏思考档位全词汇表(唯一事实源):'' = 自动(模型默认档 → high),
|
|
10
|
+
* 其余为各适配器通用档位词汇表(deepseek 认 'off',OpenAI 系是 'none')。
|
|
11
|
+
* schema(config/settings)、运行时解析(settings.resolveSettings)与 RPC
|
|
12
|
+
* 写入门(stats.settings-set)共用,勿在别处再抄字面量表。
|
|
13
|
+
*/
|
|
14
|
+
export const EFFORT_CHOICES = ['', 'off', 'none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
8
15
|
export const memorySchema = Schema.object({
|
|
9
16
|
dataDir: Schema.string().default(''),
|
|
10
17
|
family: Schema.union(['auto', 'chat', 'work']).default('auto'),
|
|
@@ -64,13 +71,14 @@ export const memorySchema = Schema.object({
|
|
|
64
71
|
// 蒸馏思考档位:'' = 自动(按模型能力解析:模型默认档 → high,见 llm.ts decideSendableEffort);
|
|
65
72
|
// 显式值仅在该模型声明支持时发送(跨供应商 effort 词汇表不同:deepseek 认 'off',
|
|
66
73
|
// openai 系是 'none',未声明档位的模型不传)。旧默认 'off' 在非 deepseek 模型上必炸(400/本地拒绝)
|
|
67
|
-
reasoningEffort: Schema.union([
|
|
74
|
+
reasoningEffort: Schema.union([...EFFORT_CHOICES]).default(''),
|
|
68
75
|
temperature: Schema.number().min(0).max(2).default(0.3),
|
|
69
76
|
// 模型上下文 1M token,日常压到 ~700k 使用(中文按 1 字≈1 token 保守折算)
|
|
70
77
|
maxInputChars: Schema.number().min(1000).max(1_000_000).default(700_000),
|
|
71
78
|
timeoutMs: Schema.number().min(1000).max(600_000).default(120_000),
|
|
72
79
|
}),
|
|
73
80
|
tools: Schema.boolean().default(true),
|
|
81
|
+
benchControl: Schema.boolean().default(false),
|
|
74
82
|
});
|
|
75
83
|
export function resolveDataDir(cfg) {
|
|
76
84
|
if (cfg.dataDir)
|
package/dist/hooks/recall.d.ts
CHANGED
|
@@ -30,9 +30,32 @@ import type { MemoryLogger } from '../types.js';
|
|
|
30
30
|
export declare function buildRecallQuery(messages: Array<{
|
|
31
31
|
content: unknown;
|
|
32
32
|
}>, tailMessages?: number, maxChars?: number): string;
|
|
33
|
+
/** 单会话召回统计(悬浮卡信息区数据源;每轮 O(1) 记账,agent/disposed 清理)。
|
|
34
|
+
* 口径声明:这是"注入统计"而非 bench 的离线 recall@k——运行时没有 ground truth,
|
|
35
|
+
* 命中率 = hitTurns / injectedTurns(发生过检索的轮次中命中 ≥1 条的占比)。 */
|
|
36
|
+
export interface RecallSessionStats {
|
|
37
|
+
/** 发生过召回检索的轮次数(含零命中与超时)。 */
|
|
38
|
+
injectedTurns: number;
|
|
39
|
+
/** 命中(≥1 条)轮次数。 */
|
|
40
|
+
hitTurns: number;
|
|
41
|
+
/** 累计命中条数(预算截断前)。 */
|
|
42
|
+
totalHits: number;
|
|
43
|
+
/** 总预算超时跳过次数。 */
|
|
44
|
+
timeouts: number;
|
|
45
|
+
/** 最近一轮命中条数(0 = 零命中/超时;工具指南门控沿用此信号)。 */
|
|
46
|
+
lastHits: number;
|
|
47
|
+
/** 最近一轮检索耗时 ms。 */
|
|
48
|
+
lastDurationMs: number;
|
|
49
|
+
/** 最近一次记账时间(LRU 清理依据)。 */
|
|
50
|
+
updatedAt: number;
|
|
51
|
+
}
|
|
52
|
+
/** 新建零值统计(首次出现的会话)。 */
|
|
53
|
+
export declare function emptyRecallStats(now?: number): RecallSessionStats;
|
|
33
54
|
export interface RecallHooks {
|
|
34
55
|
/** 管线更新后调用:画像/场景缓存立即失效并异步刷新。 */
|
|
35
56
|
invalidateProfile(): void;
|
|
57
|
+
/** 会话召回统计只读视图(未发生过检索的会话返回 undefined)。 */
|
|
58
|
+
stats(sessionId: string): RecallSessionStats | undefined;
|
|
36
59
|
}
|
|
37
60
|
export declare function registerRecall(ctx: Context, cfg: MemoryConfig, stores: {
|
|
38
61
|
l1: L1Store;
|
package/dist/hooks/recall.js
CHANGED
|
@@ -35,9 +35,21 @@ const MEMORY_TOOLS_GUIDE = `<memory-tools-guide>
|
|
|
35
35
|
注:若当前环境限制直接调用工具(如仅允许代码执行入口),请经由该环境的工具调用机制
|
|
36
36
|
(如 run_code 程序内)使用以上记忆工具。
|
|
37
37
|
</memory-tools-guide>`;
|
|
38
|
+
/** 新建零值统计(首次出现的会话)。 */
|
|
39
|
+
export function emptyRecallStats(now = Date.now()) {
|
|
40
|
+
return { injectedTurns: 0, hitTurns: 0, totalHits: 0, timeouts: 0, lastHits: 0, lastDurationMs: 0, updatedAt: now };
|
|
41
|
+
}
|
|
38
42
|
export function registerRecall(ctx, cfg, stores, logger, live, modes) {
|
|
39
|
-
/** 每 agent
|
|
40
|
-
const
|
|
43
|
+
/** 每 agent 召回统计(工具指南门控读 lastHits;悬浮卡信息区读全量计数)。 */
|
|
44
|
+
const recallStats = new Map();
|
|
45
|
+
const statFor = (id) => {
|
|
46
|
+
let s = recallStats.get(id);
|
|
47
|
+
if (!s) {
|
|
48
|
+
s = emptyRecallStats();
|
|
49
|
+
recallStats.set(id, s);
|
|
50
|
+
}
|
|
51
|
+
return s;
|
|
52
|
+
};
|
|
41
53
|
// 画像/场景导航按族缓存(分族隔离:注入时按会话档位选族)
|
|
42
54
|
const profileCache = {
|
|
43
55
|
chat: { persona: '', nav: '' },
|
|
@@ -65,9 +77,9 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes) {
|
|
|
65
77
|
const invalidateProfile = () => {
|
|
66
78
|
void refreshProfile();
|
|
67
79
|
};
|
|
68
|
-
// agent
|
|
80
|
+
// agent 销毁时清掉召回统计槽
|
|
69
81
|
ctx.on('agent/disposed', (payload) => {
|
|
70
|
-
|
|
82
|
+
recallStats.delete(payload.agent.id);
|
|
71
83
|
});
|
|
72
84
|
// ── 1. pre-step 消息侧注入:记忆先行于每一条新的用户输入(ADR-0001) ──
|
|
73
85
|
// prepend 注册 + 先 next() 再改写:不劫持其他监听器(dsh-time-context 官方范式)。
|
|
@@ -86,20 +98,35 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes) {
|
|
|
86
98
|
if (!hasNewUserMessage)
|
|
87
99
|
return decision;
|
|
88
100
|
const query = buildRecallQuery(payload.messages);
|
|
89
|
-
|
|
90
|
-
if (!query)
|
|
101
|
+
// 空查询是退化轮(无用户文本),重置命中信号但不计入统计
|
|
102
|
+
if (!query) {
|
|
103
|
+
const degenerate = statFor(payload.agent.id);
|
|
104
|
+
degenerate.lastHits = 0;
|
|
91
105
|
return decision;
|
|
106
|
+
}
|
|
107
|
+
const st = statFor(payload.agent.id);
|
|
108
|
+
st.injectedTurns++;
|
|
109
|
+
st.lastHits = 0;
|
|
110
|
+
st.updatedAt = Date.now();
|
|
111
|
+
const searchStart = Date.now();
|
|
92
112
|
const hits = await raceRecallTimeout(stores.l1.search(query, cfg.recall.maxResults, {
|
|
93
113
|
scoreThreshold: cfg.recall.scoreThreshold,
|
|
94
114
|
family: mode === 'auto' ? undefined : mode,
|
|
95
115
|
// 远程嵌入 fetch 内层钳制:给 FTS 降级留出总预算内的时间(本地推理不钳)
|
|
96
116
|
embeddingTimeoutMs: RECALL_EMBED_CAP_MS,
|
|
97
117
|
}), cfg.recall.timeoutMs);
|
|
118
|
+
st.updatedAt = Date.now();
|
|
98
119
|
if (hits === undefined) {
|
|
120
|
+
st.timeouts++;
|
|
99
121
|
logger.warn('[memory] 召回超时,跳过本轮注入(不阻塞对话)');
|
|
100
122
|
return decision;
|
|
101
123
|
}
|
|
102
|
-
|
|
124
|
+
st.lastDurationMs = Date.now() - searchStart;
|
|
125
|
+
st.lastHits = hits.length;
|
|
126
|
+
if (hits.length > 0) {
|
|
127
|
+
st.hitTurns++;
|
|
128
|
+
st.totalHits += hits.length;
|
|
129
|
+
}
|
|
103
130
|
if (hits.length === 0)
|
|
104
131
|
return decision;
|
|
105
132
|
const lines = applyRecallBudget(hits.map((h) => `- [${h.scene_name ? `${h.type}|${h.scene_name}` : h.type}] ${h.content}`), { maxCharsPerMemory: cfg.recall.maxCharsPerMemory, maxTotalRecallChars: cfg.recall.maxTotalRecallChars });
|
|
@@ -156,7 +183,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes) {
|
|
|
156
183
|
const body = mode === 'auto'
|
|
157
184
|
? formatProfileAuto(profileCache.chat, profileCache.work)
|
|
158
185
|
: formatProfileSingle(profileCache[mode]);
|
|
159
|
-
const hasRecallHit = (
|
|
186
|
+
const hasRecallHit = (recallStats.get(agent.id)?.lastHits ?? 0) > 0;
|
|
160
187
|
// 指南三条件门控:工具已注册(cfg.tools)&&(稳定内容 ∥ 本轮召回命中)——
|
|
161
188
|
// 空库用户与关闭工具的用户不付这份固定 token(原版 auto-recall 同款语义)
|
|
162
189
|
if (!cfg.tools)
|
|
@@ -189,7 +216,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes) {
|
|
|
189
216
|
}
|
|
190
217
|
}
|
|
191
218
|
});
|
|
192
|
-
return { invalidateProfile };
|
|
219
|
+
return { invalidateProfile, stats: (id) => recallStats.get(id) };
|
|
193
220
|
}
|
|
194
221
|
/** auto 档 <user-persona> 内的域说明:让模型理解分块结构与两域的独立性。 */
|
|
195
222
|
const DOMAIN_HINT = '以下内容按记忆域分块:chat=用户个人画像(User Narrative Profile),work=团队工作准则(Team Operating Doctrine)。' +
|
package/dist/index.d.ts
CHANGED
|
@@ -113,6 +113,7 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
113
113
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
114
114
|
}>>;
|
|
115
115
|
tools: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
116
|
+
benchControl: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
116
117
|
}>, Schemastery.ObjectT<{
|
|
117
118
|
dataDir: import("@deepseek-ai/schemastery").default<string, string>;
|
|
118
119
|
family: import("@deepseek-ai/schemastery").default<"chat" | "work" | "auto", "chat" | "work" | "auto">;
|
|
@@ -218,5 +219,6 @@ export declare const Config: import("@deepseek-ai/schemastery").default<Schemast
|
|
|
218
219
|
timeoutMs: import("@deepseek-ai/schemastery").default<number, number>;
|
|
219
220
|
}>>;
|
|
220
221
|
tools: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
222
|
+
benchControl: import("@deepseek-ai/schemastery").default<boolean, boolean>;
|
|
221
223
|
}>>;
|
|
222
224
|
export declare function apply(ctx: Context, config: MemoryConfig): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as path from 'node:path';
|
|
20
20
|
import { memorySchema, resolveDataDir } from './config.js';
|
|
21
|
+
import { registerBenchControl } from './bench-control.js';
|
|
21
22
|
import { registerCapture } from './hooks/capture.js';
|
|
22
23
|
import { registerRecall } from './hooks/recall.js';
|
|
23
24
|
import { MemoryRunner } from './pipeline/runner.js';
|
|
@@ -297,7 +298,21 @@ export async function apply(ctx, config) {
|
|
|
297
298
|
registerMemoryRpc(ctx, config, stores, logger, {
|
|
298
299
|
degraded: () => !storageOk || db.isDegraded(),
|
|
299
300
|
pending: () => runner.pendingCount,
|
|
300
|
-
}, live, modes, dataDir, rebuild, embedManager
|
|
301
|
+
}, live, modes, dataDir, rebuild, embedManager,
|
|
302
|
+
// 悬浮卡信息区数据源(session-stats 热路径端点;全部内存读 + 索引 COUNT,零文件 I/O)
|
|
303
|
+
{
|
|
304
|
+
recallStats: (sid) => recall.stats(sid),
|
|
305
|
+
runnerView: (sid, mode) => runner.sessionView(sid, mode),
|
|
306
|
+
l0Count: (sid) => stores.l0.countBySession(sid),
|
|
307
|
+
capabilities: () => db.getCapabilities(),
|
|
308
|
+
});
|
|
309
|
+
// bench 控制服务(config.benchControl 门控,默认关):仅基准/调试部署注册,
|
|
310
|
+
// 供同进程的 bench-runner lifecycle 赛道触发 rebuild / 设置会话档位
|
|
311
|
+
// (宿主侧 RPC 无 call(),见 bench-control.ts)
|
|
312
|
+
if (config.benchControl && rebuild) {
|
|
313
|
+
const disposeBench = registerBenchControl(ctx, rebuild, modes, logger);
|
|
314
|
+
ctx.effect(() => () => disposeBench());
|
|
315
|
+
}
|
|
301
316
|
logger.info(`[memory] L0~L3 分层蒸馏记忆插件就绪(L1 记忆 ${storageOk ? stores.l1.size : 0} 条 | 捕获=${storageOk && config.capture.enabled} | 蒸馏=${storageOk && config.extract.enabled} | 召回=${config.recall.enabled})`);
|
|
302
317
|
// 停机顺序(M7):置停机标志(后台 embeddings 不再发起)→ 停蒸馏取新任务 →
|
|
303
318
|
// 冲刷 L0 串行链(排队消息先落盘)→ 关库。cordis disposer 为 LIFO 逐个 await,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏用量追踪(进程内累计计数器,常开——纯内存累加,无 IO):
|
|
3
|
+
* callLLM 按层(l1-extract/l1-dedup/l2/l3)累计调用数、输入字符、输出/思考 token。
|
|
4
|
+
*
|
|
5
|
+
* 用途:基准的「记忆开销」记账(效率三角之一)——L1/L2/L3 蒸馏消耗摊到每条
|
|
6
|
+
* 捕获消息,与工作流赛道已测的"记忆节省"(B 组重新探索的 token)配成完整 ROI。
|
|
7
|
+
* 暴露面:bench 控制服务 getDistillUsage()(config.benchControl 门控)。
|
|
8
|
+
*
|
|
9
|
+
* 输入侧说明:dsh llm 流的 usage 块只携带 output/reasoning token,输入 token
|
|
10
|
+
* 拿不到——按仓库既有口径记输入**字符**(中文 1 字 ≈ 1 token 保守折算,
|
|
11
|
+
* 见 config.ts maxInputChars 注释),报告侧如实标注。
|
|
12
|
+
*/
|
|
13
|
+
export type DistillLayer = 'l1-extract' | 'l1-dedup' | 'l2' | 'l3';
|
|
14
|
+
export interface DistillLayerUsage {
|
|
15
|
+
calls: number;
|
|
16
|
+
failures: number;
|
|
17
|
+
inputChars: number;
|
|
18
|
+
outputTokens: number;
|
|
19
|
+
reasoningTokens: number;
|
|
20
|
+
}
|
|
21
|
+
export interface DistillUsageSnapshot {
|
|
22
|
+
layers: Record<string, DistillLayerUsage>;
|
|
23
|
+
}
|
|
24
|
+
/** 单次调用用量累计(callLLM 出口调用;inputChars 为实际发送的用户 prompt 字符)。 */
|
|
25
|
+
export declare function recordDistillCall(layer: DistillLayer, inputChars: number, outputTokens: number, reasoningTokens: number, failed: boolean): void;
|
|
26
|
+
/** 深拷贝快照(消费方拿到后不受后续累计影响)。 */
|
|
27
|
+
export declare function snapshotDistillUsage(): DistillUsageSnapshot;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蒸馏用量追踪(进程内累计计数器,常开——纯内存累加,无 IO):
|
|
3
|
+
* callLLM 按层(l1-extract/l1-dedup/l2/l3)累计调用数、输入字符、输出/思考 token。
|
|
4
|
+
*
|
|
5
|
+
* 用途:基准的「记忆开销」记账(效率三角之一)——L1/L2/L3 蒸馏消耗摊到每条
|
|
6
|
+
* 捕获消息,与工作流赛道已测的"记忆节省"(B 组重新探索的 token)配成完整 ROI。
|
|
7
|
+
* 暴露面:bench 控制服务 getDistillUsage()(config.benchControl 门控)。
|
|
8
|
+
*
|
|
9
|
+
* 输入侧说明:dsh llm 流的 usage 块只携带 output/reasoning token,输入 token
|
|
10
|
+
* 拿不到——按仓库既有口径记输入**字符**(中文 1 字 ≈ 1 token 保守折算,
|
|
11
|
+
* 见 config.ts maxInputChars 注释),报告侧如实标注。
|
|
12
|
+
*/
|
|
13
|
+
const counters = new Map();
|
|
14
|
+
function bucketOf(layer) {
|
|
15
|
+
let b = counters.get(layer);
|
|
16
|
+
if (!b) {
|
|
17
|
+
b = { calls: 0, failures: 0, inputChars: 0, outputTokens: 0, reasoningTokens: 0 };
|
|
18
|
+
counters.set(layer, b);
|
|
19
|
+
}
|
|
20
|
+
return b;
|
|
21
|
+
}
|
|
22
|
+
/** 单次调用用量累计(callLLM 出口调用;inputChars 为实际发送的用户 prompt 字符)。 */
|
|
23
|
+
export function recordDistillCall(layer, inputChars, outputTokens, reasoningTokens, failed) {
|
|
24
|
+
const b = bucketOf(layer);
|
|
25
|
+
b.calls++;
|
|
26
|
+
if (failed)
|
|
27
|
+
b.failures++;
|
|
28
|
+
b.inputChars += Math.max(0, Math.round(inputChars));
|
|
29
|
+
b.outputTokens += Math.max(0, Math.round(outputTokens));
|
|
30
|
+
b.reasoningTokens += Math.max(0, Math.round(reasoningTokens));
|
|
31
|
+
}
|
|
32
|
+
/** 深拷贝快照(消费方拿到后不受后续累计影响)。 */
|
|
33
|
+
export function snapshotDistillUsage() {
|
|
34
|
+
const layers = {};
|
|
35
|
+
for (const [layer, b] of counters) {
|
|
36
|
+
layers[layer] = { ...b };
|
|
37
|
+
}
|
|
38
|
+
return { layers };
|
|
39
|
+
}
|