dsh-layered-memory 0.8.7 → 0.8.9
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 +47 -4
- package/README.md +51 -4
- package/assets/changelog/0.8.9/01-panel.png +0 -0
- package/assets/changelog/0.8.9/02-halo.png +0 -0
- package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
- package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
- package/dist/client.js +3542 -3293
- package/dist/config.d.ts +286 -4
- package/dist/config.js +27 -0
- package/dist/contract.d.ts +652 -0
- package/dist/contract.js +1 -0
- package/dist/hooks/recall.d.ts +13 -18
- package/dist/hooks/recall.js +143 -20
- package/dist/index.d.ts +268 -4
- package/dist/index.js +9 -3
- package/dist/llm-usage.d.ts +2 -1
- package/dist/llm.d.ts +63 -11
- package/dist/llm.js +165 -15
- package/dist/pipeline/rebuild.d.ts +2 -21
- package/dist/pipeline/runner.d.ts +3 -3
- package/dist/pipeline/runner.js +52 -10
- package/dist/settings.d.ts +25 -29
- package/dist/settings.js +107 -1
- package/dist/stats.d.ts +9 -26
- package/dist/stats.js +149 -22
- package/dist/store/cost-ledger.d.ts +78 -0
- package/dist/store/cost-ledger.js +173 -0
- package/dist/store/download-queue.d.ts +2 -20
- package/dist/store/embedding-source.d.ts +2 -52
- package/dist/store/occupancy.d.ts +30 -0
- package/dist/store/occupancy.js +134 -0
- package/dist/store/runtime-installer.d.ts +2 -13
- package/dist/store/sqlite.d.ts +11 -61
- package/dist/store/sqlite.js +15 -150
- package/dist/token-cost.d.ts +3 -72
- package/dist/util/context-occupancy.d.ts +68 -0
- package/dist/util/context-occupancy.js +92 -0
- package/package.json +8 -4
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ import { SessionModeStore } from './store/session-modes.js';
|
|
|
39
39
|
import { StateStore } from './store/state.js';
|
|
40
40
|
import { registerMemoryTools } from './tools/index.js';
|
|
41
41
|
import { errDetail, withFileLog } from './util/filelog.js';
|
|
42
|
-
import { resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
42
|
+
import { buildRouteChain, resolveModelRoute, invalidateEffortCache } from './llm.js';
|
|
43
43
|
import { effectiveCfg } from './pipeline/runner.js';
|
|
44
44
|
import { initTokenCost, resetTokenCost } from './token-cost.js';
|
|
45
45
|
export const name = 'dsh-memory-plugin';
|
|
@@ -170,8 +170,11 @@ export async function apply(ctx, config) {
|
|
|
170
170
|
// 蒸馏模型路由:启动期解析一次并记录(路由错误是最难事后排查的问题之一);
|
|
171
171
|
// 用运行时调参视图解析——用户已用 UI 覆盖蒸馏模型时,日志反映实际路由
|
|
172
172
|
try {
|
|
173
|
-
const
|
|
174
|
-
|
|
173
|
+
const cfgView = effectiveCfg(config, live);
|
|
174
|
+
const route = await resolveModelRoute(ctx, cfgView);
|
|
175
|
+
// 链长度按同一解析口径计(相同条目会被去重),排障时无需 --dump-config 即可确认回退链生效
|
|
176
|
+
const chain = buildRouteChain(route, cfgView.llm.fallbacks, cfgView.llm.reasoningEffort);
|
|
177
|
+
logger.info(`[memory] 蒸馏模型路由: ${route.provider}/${route.model}${chain.length > 1 ? `(+${chain.length - 1} 回退)` : ''}`);
|
|
175
178
|
}
|
|
176
179
|
catch (err) {
|
|
177
180
|
logger.warn(`[memory] 蒸馏模型路由解析失败: ${errDetail(err)}`);
|
|
@@ -304,6 +307,9 @@ export async function apply(ctx, config) {
|
|
|
304
307
|
// 悬浮卡信息区数据源(session-stats 热路径端点;全部内存读 + 索引 COUNT,零文件 I/O)
|
|
305
308
|
{
|
|
306
309
|
recallStats: (sid) => recall.stats(sid),
|
|
310
|
+
memoryOccupancy: (sid) => recall.occupancy(sid),
|
|
311
|
+
profileEstimate: (sid) => recall.estimateProfileTokens(sid),
|
|
312
|
+
recallEstimate: (sid) => recall.estimateRecallTokens(sid),
|
|
307
313
|
runnerView: (sid, mode) => runner.sessionView(sid, mode),
|
|
308
314
|
l0Count: (sid) => stores.l0.countBySession(sid),
|
|
309
315
|
capabilities: () => db.getCapabilities(),
|
package/dist/llm-usage.d.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* 拿不到——按仓库既有口径记输入**字符**(中文 1 字 ≈ 1 token 保守折算,
|
|
11
11
|
* 见 config.ts maxInputChars 注释),报告侧如实标注。
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
import type { DistillLayer } from './contract.js';
|
|
14
|
+
export type { DistillLayer } from './contract.js';
|
|
14
15
|
export interface DistillLayerUsage {
|
|
15
16
|
calls: number;
|
|
16
17
|
failures: number;
|
package/dist/llm.d.ts
CHANGED
|
@@ -24,21 +24,28 @@ export declare const LAYER_MAX_TOKENS_DEDUP = 8000;
|
|
|
24
24
|
export declare const LAYER_MAX_TOKENS_L2 = 32000;
|
|
25
25
|
/** L3 画像(完整 persona 文档)。 */
|
|
26
26
|
export declare const LAYER_MAX_TOKENS_L3 = 16000;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/** 分层输出预算键(已迁入契约 src/contract.ts;import type 供本地使用,re-export 不断裂既有引用)。 */
|
|
28
|
+
import type { DistillBudgetLayer, LayerRouteKey, StaticFallbackEntry } from './contract.js';
|
|
29
|
+
export type { DistillBudgetLayer } from './contract.js';
|
|
29
30
|
/** 各层内置默认预算(设置页"0 = 跟随默认"的默认值来源)。 */
|
|
30
31
|
export declare const LAYER_DEFAULT_BUDGETS: Record<DistillBudgetLayer, number>;
|
|
31
|
-
/**
|
|
32
|
-
|
|
33
|
-
* 设置页 distillBudgets 注入,0/缺省 = 跟随)→ 内置默认 → 思考档放大
|
|
34
|
-
* (high/xhigh/max ×4,reasoning 计入输出预算的历史事故防线)。
|
|
35
|
-
*/
|
|
36
|
-
export declare function resolveLayerTokens(cfg: {
|
|
32
|
+
/** resolveLayerTokens/layerEffortTrigger 需要的最小 cfg 视图(smoke 决策表用窄对象即可构造)。 */
|
|
33
|
+
export interface LayerRouteCfgView {
|
|
37
34
|
llm: {
|
|
38
35
|
reasoningEffort: string;
|
|
36
|
+
primaryEffort?: string;
|
|
37
|
+
layerRoutes?: Partial<Record<LayerRouteKey, StaticFallbackEntry[]>>;
|
|
38
|
+
layerChainsRuntime?: Partial<Record<LayerRouteKey, StaticFallbackEntry[]>>;
|
|
39
39
|
budgets?: Partial<Record<DistillBudgetLayer, number>>;
|
|
40
40
|
};
|
|
41
|
-
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 解析某蒸馏层的生效输出预算:运行时覆盖(cfg.llm.budgets,由 effectiveCfg 从
|
|
44
|
+
* 设置页 distillBudgets 注入,0/缺省 = 跟随)→ 内置默认 → 思考档放大
|
|
45
|
+
* (high/xhigh/max ×4,reasoning 计入输出预算的历史事故防线)。放大触发档位
|
|
46
|
+
* 跟层走(#34 D8):层链头档位候选 > 全局主路由档位候选(primaryEffort > 静态全局)。
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveLayerTokens(cfg: LayerRouteCfgView, layer: DistillBudgetLayer): number;
|
|
42
49
|
/**
|
|
43
50
|
* 高思考档集合(输出预算 ×4 的档位):阶段侧 layerMaxTokens 与 callLLM 的
|
|
44
51
|
* 自动档防线共用同一张表——此前两处字面量表分叉(防线漏 xhigh),显式 xhigh
|
|
@@ -55,6 +62,43 @@ export declare function resolveModelRoute(ctx: Context, cfg: MemoryConfig): Prom
|
|
|
55
62
|
provider: string;
|
|
56
63
|
model: string;
|
|
57
64
|
}>;
|
|
65
|
+
/** 回退链条目(配置形态;reasoningEffort 为该路由的档位覆盖,'' = 跟随全局)。 */
|
|
66
|
+
export interface FallbackRouteEntry {
|
|
67
|
+
provider: string;
|
|
68
|
+
model: string;
|
|
69
|
+
reasoningEffort?: string;
|
|
70
|
+
}
|
|
71
|
+
/** 链上单条路由(档位候选已解析:条目非空 > 全局静态;发送前仍过能力钳制)。 */
|
|
72
|
+
export interface DistillRoute {
|
|
73
|
+
provider: string;
|
|
74
|
+
model: string;
|
|
75
|
+
effort: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 组装蒸馏路由链(纯决策,smoke 决策表缝):主路由在前、回退条目按配置顺序在后。
|
|
79
|
+
* provider/model 缺失的条目剔除;与主路由或先前条目完全相同(provider+model)的
|
|
80
|
+
* 条目跳过——注定失败的重复尝试不值得占位。每条路由携带生效档位候选:
|
|
81
|
+
* 主路由可带显式档位(运行时统一链注入 primaryEffort),条目档位非空覆盖全局。
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildRouteChain(primary: {
|
|
84
|
+
provider: string;
|
|
85
|
+
model: string;
|
|
86
|
+
effort?: string;
|
|
87
|
+
}, fallbacks: FallbackRouteEntry[] | undefined, globalEffort: string): DistillRoute[];
|
|
88
|
+
/** DistillLayer(调用点四键)→ 路由层键(三键):l1-extract/l1-dedup 同属 l1。 */
|
|
89
|
+
export declare function layerKeyFor(layer: DistillLayer): LayerRouteKey;
|
|
90
|
+
/** 该层的预算放大触发档位(D8):层链头档位候选 > 全局主路由档位候选(primaryEffort > 静态全局)。 */
|
|
91
|
+
export declare function layerEffortTrigger(cfg: LayerRouteCfgView, key: LayerRouteKey): string;
|
|
92
|
+
/**
|
|
93
|
+
* 解析某次蒸馏调用的实际路由链(callLLM 入口):有层标签且该层配了层链 → 层链
|
|
94
|
+
* 完整替换(buildRouteChain 复用:头行在前、条目去重、档位三级候选);否则现行
|
|
95
|
+
* 全局解析(主路由既有优先级 + fallbacks,语义一个比特不动)。layer 缺省
|
|
96
|
+
* (bench/测试缝)= 全局解析。
|
|
97
|
+
*/
|
|
98
|
+
export declare function resolveLayerRoutes(ctx: Context, cfg: MemoryConfig, layer?: DistillLayer): Promise<DistillRoute[]>;
|
|
99
|
+
/** 层链解析的同步半边(llm-providers 视图与 resolveLayerRoutes 共用一条真值路径):
|
|
100
|
+
* 该层配了有效层链 → 完整链;null = 该层跟随全局解析。 */
|
|
101
|
+
export declare function layerChainOrNull(cfg: LayerRouteCfgView, key: LayerRouteKey): DistillRoute[] | null;
|
|
58
102
|
export interface ModelEffortInfo {
|
|
59
103
|
/** 模型可设置的思考档位 id(适配器声明;空 = 未声明/不可设置) */
|
|
60
104
|
efforts: string[];
|
|
@@ -65,6 +109,12 @@ export interface ModelEffortInfo {
|
|
|
65
109
|
export declare function invalidateEffortCache(): void;
|
|
66
110
|
/** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
|
|
67
111
|
export declare function resolveModelEfforts(ctx: Context, provider: string, model: string): Promise<ModelEffortInfo | null>;
|
|
112
|
+
/**
|
|
113
|
+
* 探询某模型的上下文窗口容量(adapter 声明的 provider-owned capacity)。
|
|
114
|
+
* 与 effortCache 同源同失效策略(invalidateEffortCache 一并清空);
|
|
115
|
+
* 仅用于占用指示器的分母展示——分母必须与官方环同源(模型声明值),禁止 client 自估。
|
|
116
|
+
*/
|
|
117
|
+
export declare function resolveModelContextWindow(ctx: Context, provider: string, model: string): Promise<number | null>;
|
|
68
118
|
export type EffortDecisionReason = 'supported' | 'auto-default' | 'auto-high' | 'alias-none' | 'unsupported' | 'no-efforts' | 'no-capability';
|
|
69
119
|
export interface EffortDecision {
|
|
70
120
|
/** 实际发送的档位;'' = 不发送(跟随模型默认) */
|
|
@@ -76,8 +126,10 @@ export declare function decideSendableEffort(cap: ModelEffortInfo | null, cfgEff
|
|
|
76
126
|
/** 探询 + 决策 + 一次性告警(不支持/未声明时提示降级,不刷屏)。 */
|
|
77
127
|
export declare function planDistillEffort(ctx: Context, provider: string, model: string, cfgEffort: string, logger?: MemoryLogger): Promise<EffortDecision>;
|
|
78
128
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
129
|
+
* 一次完整蒸馏调用(带回退链,ADR-0004):按路由链(主路由 + llm.fallbacks)逐条
|
|
130
|
+
* 尝试,返回首个成功路由的输出。失败(error/aborted finish、网络异常、空输出)
|
|
131
|
+
* 降级下一条;调用方主动取消(signal 已中止)原样上抛不降级;全部失败抛最后一个
|
|
132
|
+
* 错误,由调用方兜底(runner 的按会话指数退避接管重试节奏)。
|
|
81
133
|
*
|
|
82
134
|
* 文本只从 block-end(协议保证携带**组装完成的整块**)取;text-delta 仅在
|
|
83
135
|
* 适配器异常地没有发 block-end 时兜底。两者都累计会把输出翻倍。
|
package/dist/llm.js
CHANGED
|
@@ -22,11 +22,13 @@ export const LAYER_DEFAULT_BUDGETS = {
|
|
|
22
22
|
/**
|
|
23
23
|
* 解析某蒸馏层的生效输出预算:运行时覆盖(cfg.llm.budgets,由 effectiveCfg 从
|
|
24
24
|
* 设置页 distillBudgets 注入,0/缺省 = 跟随)→ 内置默认 → 思考档放大
|
|
25
|
-
* (high/xhigh/max ×4,reasoning
|
|
25
|
+
* (high/xhigh/max ×4,reasoning 计入输出预算的历史事故防线)。放大触发档位
|
|
26
|
+
* 跟层走(#34 D8):层链头档位候选 > 全局主路由档位候选(primaryEffort > 静态全局)。
|
|
26
27
|
*/
|
|
27
28
|
export function resolveLayerTokens(cfg, layer) {
|
|
28
29
|
const override = cfg.llm.budgets?.[layer];
|
|
29
|
-
|
|
30
|
+
const key = layer === 'l2' ? 'l2' : layer === 'l3' ? 'l3' : 'l1';
|
|
31
|
+
return layerMaxTokens(override && override > 0 ? override : LAYER_DEFAULT_BUDGETS[layer], layerEffortTrigger(cfg, key));
|
|
30
32
|
}
|
|
31
33
|
/**
|
|
32
34
|
* 高思考档集合(输出预算 ×4 的档位):阶段侧 layerMaxTokens 与 callLLM 的
|
|
@@ -54,11 +56,98 @@ export async function resolveModelRoute(ctx, cfg) {
|
|
|
54
56
|
}
|
|
55
57
|
throw new Error('无法解析蒸馏模型路由:请在插件 config 中配置 llm.provider / llm.model,或确保存在默认模型选择');
|
|
56
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* 组装蒸馏路由链(纯决策,smoke 决策表缝):主路由在前、回退条目按配置顺序在后。
|
|
61
|
+
* provider/model 缺失的条目剔除;与主路由或先前条目完全相同(provider+model)的
|
|
62
|
+
* 条目跳过——注定失败的重复尝试不值得占位。每条路由携带生效档位候选:
|
|
63
|
+
* 主路由可带显式档位(运行时统一链注入 primaryEffort),条目档位非空覆盖全局。
|
|
64
|
+
*/
|
|
65
|
+
export function buildRouteChain(primary, fallbacks, globalEffort) {
|
|
66
|
+
const routes = [{ ...primary, effort: primary.effort || globalEffort }];
|
|
67
|
+
const seen = new Set([`${primary.provider}::${primary.model}`]);
|
|
68
|
+
for (const f of fallbacks ?? []) {
|
|
69
|
+
if (!f.provider || !f.model)
|
|
70
|
+
continue;
|
|
71
|
+
const key = `${f.provider}::${f.model}`;
|
|
72
|
+
if (seen.has(key))
|
|
73
|
+
continue;
|
|
74
|
+
seen.add(key);
|
|
75
|
+
routes.push({ provider: f.provider, model: f.model, effort: f.reasoningEffort || globalEffort });
|
|
76
|
+
}
|
|
77
|
+
return routes;
|
|
78
|
+
}
|
|
79
|
+
// ── 按层独立路由(#34 / ADR-0005):层内三级 运行时层链 > 静态层链 > 全局解析 ──
|
|
80
|
+
// 层链非空即完整替换该层解析(该层主路由与回退都归层链管,全局链对该层不参与);
|
|
81
|
+
// 档位是全局偏好轴:层链空档位条目/头行仍回退全局档位(primaryEffort 之后的
|
|
82
|
+
// cfg.llm.reasoningEffort 语义照旧)。pin 只废运行时侧(effectiveCfg 不注入),
|
|
83
|
+
// 静态层链天然穿透——与回退链"静态配置不构成 pin 绕过"先例同源。
|
|
84
|
+
/** DistillLayer(调用点四键)→ 路由层键(三键):l1-extract/l1-dedup 同属 l1。 */
|
|
85
|
+
export function layerKeyFor(layer) {
|
|
86
|
+
return layer === 'l2' ? 'l2' : layer === 'l3' ? 'l3' : 'l1';
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 取某层的生效层链(运行时优先)。头行残缺(provider/model 缺失——手写 YAML 错误
|
|
90
|
+
* 或防御性解析后的空链头)视为该层未配置、回退全局解析:路由配置错误不致该层
|
|
91
|
+
* 蒸馏失产,与回退链"条目缺失剔除"同一防御姿态。
|
|
92
|
+
*/
|
|
93
|
+
function layerChainOf(cfg, key) {
|
|
94
|
+
const rt = cfg.llm.layerChainsRuntime?.[key];
|
|
95
|
+
if (rt?.length && rt[0].provider && rt[0].model)
|
|
96
|
+
return rt;
|
|
97
|
+
const st = cfg.llm.layerRoutes?.[key];
|
|
98
|
+
if (st?.length && st[0].provider && st[0].model)
|
|
99
|
+
return st;
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
/** 该层的预算放大触发档位(D8):层链头档位候选 > 全局主路由档位候选(primaryEffort > 静态全局)。 */
|
|
103
|
+
export function layerEffortTrigger(cfg, key) {
|
|
104
|
+
const chain = layerChainOf(cfg, key);
|
|
105
|
+
return chain
|
|
106
|
+
? chain[0].reasoningEffort || cfg.llm.primaryEffort || cfg.llm.reasoningEffort
|
|
107
|
+
: cfg.llm.primaryEffort || cfg.llm.reasoningEffort;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* 解析某次蒸馏调用的实际路由链(callLLM 入口):有层标签且该层配了层链 → 层链
|
|
111
|
+
* 完整替换(buildRouteChain 复用:头行在前、条目去重、档位三级候选);否则现行
|
|
112
|
+
* 全局解析(主路由既有优先级 + fallbacks,语义一个比特不动)。layer 缺省
|
|
113
|
+
* (bench/测试缝)= 全局解析。
|
|
114
|
+
*/
|
|
115
|
+
export async function resolveLayerRoutes(ctx, cfg, layer) {
|
|
116
|
+
const key = layer ? layerKeyFor(layer) : undefined;
|
|
117
|
+
const lr = key ? layerChainOrNull(cfg, key) : null;
|
|
118
|
+
if (lr)
|
|
119
|
+
return lr;
|
|
120
|
+
const primary = await resolveModelRoute(ctx, cfg);
|
|
121
|
+
return buildRouteChain(
|
|
122
|
+
// 主路由显式档位来自运行时统一链(primaryEffort,'' = 跟随全局静态)
|
|
123
|
+
{ provider: primary.provider, model: primary.model, effort: cfg.llm.primaryEffort || '' }, cfg.llm.fallbacks, cfg.llm.reasoningEffort);
|
|
124
|
+
}
|
|
125
|
+
/** 层链解析的同步半边(llm-providers 视图与 resolveLayerRoutes 共用一条真值路径):
|
|
126
|
+
* 该层配了有效层链 → 完整链;null = 该层跟随全局解析。 */
|
|
127
|
+
export function layerChainOrNull(cfg, key) {
|
|
128
|
+
const chain = layerChainOf(cfg, key);
|
|
129
|
+
if (!chain)
|
|
130
|
+
return null;
|
|
131
|
+
return buildRouteChain({ provider: chain[0].provider, model: chain[0].model, effort: chain[0].reasoningEffort || '' }, chain.slice(1), cfg.llm.reasoningEffort);
|
|
132
|
+
}
|
|
133
|
+
/** 单路由持续失败的一次性告警去重表(effortWarned 同款;拓扑变化随能力缓存一起失效)。 */
|
|
134
|
+
const routeDeadWarned = new Set();
|
|
135
|
+
function warnRouteDeadOnce(route, logger) {
|
|
136
|
+
if (!logger)
|
|
137
|
+
return;
|
|
138
|
+
const key = `${route.provider}::${route.model}`;
|
|
139
|
+
if (routeDeadWarned.has(key))
|
|
140
|
+
return;
|
|
141
|
+
routeDeadWarned.add(key);
|
|
142
|
+
logger.warn(`[memory] 蒸馏路由 ${route.provider}/${route.model} 失败(每路由仅告警一次;逐次失败原因与降级去向见后续日志)`);
|
|
143
|
+
}
|
|
57
144
|
const effortCache = new Map();
|
|
58
145
|
/** 清空能力缓存(llm/adapters-updated 时调用:供应商增删/改配置后重新探询)。 */
|
|
59
146
|
export function invalidateEffortCache() {
|
|
60
147
|
effortCache.clear();
|
|
148
|
+
contextWindowCache.clear();
|
|
61
149
|
effortWarned.clear();
|
|
150
|
+
routeDeadWarned.clear();
|
|
62
151
|
}
|
|
63
152
|
/** 探询某模型的思考档位能力;失败返回 null(调用方保持旧发送行为,不改判)。 */
|
|
64
153
|
export async function resolveModelEfforts(ctx, provider, model) {
|
|
@@ -84,6 +173,30 @@ export async function resolveModelEfforts(ctx, provider, model) {
|
|
|
84
173
|
return null; // 不缓存失败:路由尚未注册等瞬时态,下次调用重试
|
|
85
174
|
}
|
|
86
175
|
}
|
|
176
|
+
/** (provider, model) → 上下文窗口 token 数;advisory,未声明/失败为 null。 */
|
|
177
|
+
const contextWindowCache = new Map();
|
|
178
|
+
/**
|
|
179
|
+
* 探询某模型的上下文窗口容量(adapter 声明的 provider-owned capacity)。
|
|
180
|
+
* 与 effortCache 同源同失效策略(invalidateEffortCache 一并清空);
|
|
181
|
+
* 仅用于占用指示器的分母展示——分母必须与官方环同源(模型声明值),禁止 client 自估。
|
|
182
|
+
*/
|
|
183
|
+
export async function resolveModelContextWindow(ctx, provider, model) {
|
|
184
|
+
const key = `${provider}::${model}`;
|
|
185
|
+
if (contextWindowCache.has(key))
|
|
186
|
+
return contextWindowCache.get(key) ?? null;
|
|
187
|
+
try {
|
|
188
|
+
if (typeof ctx.llm?.resolveModelInfo !== 'function')
|
|
189
|
+
return null;
|
|
190
|
+
const info = await ctx.llm.resolveModelInfo(provider, model);
|
|
191
|
+
const win = info.context?.contextWindow;
|
|
192
|
+
const val = typeof win === 'number' && Number.isFinite(win) && win > 0 ? Math.floor(win) : null;
|
|
193
|
+
contextWindowCache.set(key, val);
|
|
194
|
+
return val;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return null; // 不缓存失败:下次调用重试
|
|
198
|
+
}
|
|
199
|
+
}
|
|
87
200
|
/** 纯决策:配置档位 + 模型能力 → 实际发送值(callLLM 与 settings-get 共用)。 */
|
|
88
201
|
export function decideSendableEffort(cap, cfgEffort) {
|
|
89
202
|
if (!cap)
|
|
@@ -124,29 +237,59 @@ export async function planDistillEffort(ctx, provider, model, cfgEffort, logger)
|
|
|
124
237
|
return d;
|
|
125
238
|
}
|
|
126
239
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
240
|
+
* 一次完整蒸馏调用(带回退链,ADR-0004):按路由链(主路由 + llm.fallbacks)逐条
|
|
241
|
+
* 尝试,返回首个成功路由的输出。失败(error/aborted finish、网络异常、空输出)
|
|
242
|
+
* 降级下一条;调用方主动取消(signal 已中止)原样上抛不降级;全部失败抛最后一个
|
|
243
|
+
* 错误,由调用方兜底(runner 的按会话指数退避接管重试节奏)。
|
|
129
244
|
*
|
|
130
245
|
* 文本只从 block-end(协议保证携带**组装完成的整块**)取;text-delta 仅在
|
|
131
246
|
* 适配器异常地没有发 block-end 时兜底。两者都累计会把输出翻倍。
|
|
132
247
|
*/
|
|
133
248
|
export async function callLLM(ctx, cfg, opts) {
|
|
134
|
-
|
|
249
|
+
// 按层路由(#34):opts.layer 配了层链的层走层链完整替换,其余走全局解析
|
|
250
|
+
const routes = await resolveLayerRoutes(ctx, cfg, opts.layer);
|
|
251
|
+
let lastErr;
|
|
252
|
+
for (let i = 0; i < routes.length; i++) {
|
|
253
|
+
const route = routes[i];
|
|
254
|
+
try {
|
|
255
|
+
return await callRoute(ctx, cfg, opts, route);
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
// 调用方主动取消(重建取消/进程关闭)不是路由失败——不降级,原样上抛
|
|
259
|
+
if (opts.signal?.aborted)
|
|
260
|
+
throw err;
|
|
261
|
+
lastErr = err;
|
|
262
|
+
warnRouteDeadOnce(route, opts.logger);
|
|
263
|
+
const next = routes[i + 1];
|
|
264
|
+
if (next) {
|
|
265
|
+
opts.logger?.info(`[memory] 蒸馏路由降级 ${route.provider}/${route.model} → ${next.provider}/${next.model}(${errDetail(err)})`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw lastErr;
|
|
270
|
+
}
|
|
271
|
+
/** 单路由一次尝试(callLLM 循环体;每路由新建超时信号 = 各享全额 timeoutMs)。 */
|
|
272
|
+
async function callRoute(ctx, cfg, opts, route) {
|
|
273
|
+
const { provider, model } = route;
|
|
135
274
|
const signal = opts.signal ?? AbortSignal.timeout(cfg.llm.timeoutMs);
|
|
136
275
|
// 档位按模型能力决策(跨供应商 effort 兼容):不支持的档位不传 + 告警一次,
|
|
137
|
-
// 空配置 =
|
|
138
|
-
const effort = await planDistillEffort(ctx, provider, model,
|
|
276
|
+
// 空配置 = 自动(模型默认 → high);路由的档位候选已在链解析时定好(条目 > 全局)
|
|
277
|
+
const effort = await planDistillEffort(ctx, provider, model, route.effort, opts.logger);
|
|
139
278
|
// 输入预算兜底:任何蒸馏调用的用户 prompt 不超过 maxInputChars
|
|
140
279
|
// (L1 已在数据层分块,这里是 L2/L3 与异常场景的最后一道网)
|
|
141
280
|
const user = opts.user.length > cfg.llm.maxInputChars
|
|
142
281
|
? `${opts.user.slice(0, cfg.llm.maxInputChars)}\n\n[输入超出 ${cfg.llm.maxInputChars} 字符预算,已截断]`
|
|
143
282
|
: opts.user;
|
|
144
|
-
// 输出预算 ×4
|
|
145
|
-
// (
|
|
146
|
-
//
|
|
283
|
+
// 输出预算 ×4 防线跟随【实际发送】的档位:阶段侧已按该层放大触发档位
|
|
284
|
+
// (layerEffortTrigger:层链头候选 > 全局主路由候选)放大过,这里只补自动档
|
|
285
|
+
// ('' → 模型默认/高档)解析出高档时的欠放大缺口——两侧共用一张表与同一触发值,
|
|
286
|
+
// 配置本身就是高档时不再放大(防 ×16 双乘)
|
|
147
287
|
const baseMaxTokens = opts.maxTokens ?? cfg.llm.maxTokens;
|
|
148
288
|
const highTiers = HIGH_EFFORT_TIERS;
|
|
149
|
-
const
|
|
289
|
+
const triggerEffort = opts.layer
|
|
290
|
+
? layerEffortTrigger(cfg, layerKeyFor(opts.layer))
|
|
291
|
+
: cfg.llm.primaryEffort || cfg.llm.reasoningEffort;
|
|
292
|
+
const maxTokens = highTiers.includes(effort.effort) && !highTiers.includes(triggerEffort)
|
|
150
293
|
? layerMaxTokens(baseMaxTokens, 'high')
|
|
151
294
|
: baseMaxTokens;
|
|
152
295
|
const stream = ctx.llm.stream({
|
|
@@ -209,20 +352,27 @@ export async function callLLM(ctx, cfg, opts) {
|
|
|
209
352
|
opts.logger?.warn(`[memory] LLM 调用失败 ${provider}/${model}(${((Date.now() - startedAt) / 1000).toFixed(1)}s): ${errDetail(err)}`);
|
|
210
353
|
throw err;
|
|
211
354
|
}
|
|
212
|
-
if (opts.layer)
|
|
213
|
-
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, false);
|
|
214
|
-
if (opts.layer)
|
|
215
|
-
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
216
355
|
const out = (blockText || deltaText).trim();
|
|
217
356
|
if (out.length === 0) {
|
|
218
357
|
// 空输出是最难排查的失败:流正常结束但一个字没吐。必须记录 finish 原因、
|
|
219
358
|
// token 计数与块分布,才能区分"模型只产出了 reasoning"vs"服务端返回空响应"。
|
|
359
|
+
if (opts.layer)
|
|
360
|
+
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, true);
|
|
361
|
+
if (opts.layer)
|
|
362
|
+
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
220
363
|
opts.logger?.warn(`[memory] LLM 空输出 ${provider}/${model}(${((Date.now() - startedAt) / 1000).toFixed(1)}s,finish=${finishKind || '无 finish 块'}` +
|
|
221
364
|
`,输出 tokens=${outputTokens}${reasoningTokens > 0 ? `/reasoning ${reasoningTokens}` : ''},` +
|
|
222
365
|
`text-delta ${deltaBlocks} 块/${deltaText.length} 字符,reasoning ${reasoningChars} 字符,` +
|
|
223
366
|
`block-end: ${[...blockEndTypes.entries()].map(([t, n]) => `${t}×${n}`).join(', ') || '无'})` +
|
|
224
367
|
(reasoningHead ? `,reasoning 摘录: ${reasoningHead}…` : ''));
|
|
368
|
+
// 空输出按路由失败处理(#31):交给回退链降级或上抛——原先返回空串只是把失败
|
|
369
|
+
// 推迟到下游 JSON/Markdown 解析,诊断信息更差
|
|
370
|
+
throw new Error(`llm empty output: ${provider}/${model} 流正常结束但输出 0 字符`);
|
|
225
371
|
}
|
|
372
|
+
if (opts.layer)
|
|
373
|
+
recordDistillCall(opts.layer, user.length, outputTokens, reasoningTokens, false);
|
|
374
|
+
if (opts.layer)
|
|
375
|
+
recordCostCall(provider, model, opts.layer, user.length, outputTokens, reasoningTokens);
|
|
226
376
|
opts.logger?.info(`[memory] LLM 调用 ${provider}/${model}:输入 ${user.length} 字符 → 输出 ${out.length} 字符(${((Date.now() - startedAt) / 1000).toFixed(1)}s,finish=${finishKind || '无'})`);
|
|
227
377
|
return out;
|
|
228
378
|
}
|
|
@@ -15,27 +15,8 @@ export interface RebuildStores {
|
|
|
15
15
|
persona: Record<MemoryFamily, PersonaStore>;
|
|
16
16
|
state: StateStore;
|
|
17
17
|
}
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
running: boolean;
|
|
21
|
-
phase: RebuildPhase;
|
|
22
|
-
/** 已完成的会话块数 / 总块数。 */
|
|
23
|
-
done: number;
|
|
24
|
-
total: number;
|
|
25
|
-
/** L0 体量(idle 时为实时预估,运行中为快照值)。 */
|
|
26
|
-
sessionCount: number;
|
|
27
|
-
messageCount: number;
|
|
28
|
-
/** 预计 LLM 抽取调用次数(下界估算:块数与字符预算取大)。 */
|
|
29
|
-
estCalls: number;
|
|
30
|
-
/** 重建产出的 L1 记录累计条数。 */
|
|
31
|
-
recordsBuilt: number;
|
|
32
|
-
cancelRequested: boolean;
|
|
33
|
-
startedAt: number | null;
|
|
34
|
-
finishedAt: number | null;
|
|
35
|
-
error: string | null;
|
|
36
|
-
/** 归档产物名(提示用户可手工找回)。 */
|
|
37
|
-
archiveNote: string | null;
|
|
38
|
-
}
|
|
18
|
+
import type { RebuildStatus } from '../contract.js';
|
|
19
|
+
export type { RebuildPhase, RebuildStatus } from '../contract.js';
|
|
39
20
|
export interface RebuildChunk {
|
|
40
21
|
sessionId: string;
|
|
41
22
|
messages: ConversationMessage[];
|
|
@@ -36,9 +36,9 @@ export interface PipelineTask {
|
|
|
36
36
|
/** 选取下一个要执行的任务下标:最早的 live 优先,否则队首(rebuild 分块让位)。 */
|
|
37
37
|
export declare function pickNextTaskIndex(tasks: PipelineTask[]): number;
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* (空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
41
|
-
* 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
39
|
+
* 运行时调参视图:设置页运行时链(distillChain)与旧单路由/档位键、分层输出预算
|
|
40
|
+
* 可临时覆盖静态 config(空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
41
|
+
* 一层,其余键与原 cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
42
42
|
*
|
|
43
43
|
* 蒸馏模型覆盖优先级:部署静态 pin(cfg.llm.provider+model 双字段齐)不可被
|
|
44
44
|
* 运行时覆盖(部署可强制蒸馏走内网路由,防用户选择把对话外送);未 pin 时
|
package/dist/pipeline/runner.js
CHANGED
|
@@ -14,9 +14,9 @@ export function pickNextTaskIndex(tasks) {
|
|
|
14
14
|
return 0;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* (空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
19
|
-
* 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
17
|
+
* 运行时调参视图:设置页运行时链(distillChain)与旧单路由/档位键、分层输出预算
|
|
18
|
+
* 可临时覆盖静态 config(空串/0 回退静态 config / 内置默认)。浅拷贝只覆盖 llm
|
|
19
|
+
* 一层,其余键与原 cfg 共享只读引用;pipeline 全链继续收 cfg,无需感知。
|
|
20
20
|
*
|
|
21
21
|
* 蒸馏模型覆盖优先级:部署静态 pin(cfg.llm.provider+model 双字段齐)不可被
|
|
22
22
|
* 运行时覆盖(部署可强制蒸馏走内网路由,防用户选择把对话外送);未 pin 时
|
|
@@ -26,13 +26,31 @@ export function pickNextTaskIndex(tasks) {
|
|
|
26
26
|
export function effectiveCfg(cfg, live) {
|
|
27
27
|
const s = live?.get();
|
|
28
28
|
// 思考档位:设置服务在场时运行时值整体接管——'' = 自动(按模型能力解析),
|
|
29
|
-
// 不再回退静态配置("跟随配置"选项已删);静态值仅无 settings
|
|
29
|
+
// 不再回退静态配置("跟随配置"选项已删);静态值仅无 settings 服务的部署生效。
|
|
30
|
+
// 统一路由链模式下(chain 非空)旧档位键不再参与:链内每条路由自带档位候选
|
|
30
31
|
const eff = s?.reasoningEffort ?? '';
|
|
31
32
|
// 可选链防御:smoke/测试缝构造的最小 cfg 可能没有 llm 字段
|
|
32
33
|
const pinned = Boolean(cfg.llm?.provider && cfg.llm?.model);
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// 运行时统一路由链:**只认显式 distillChain**(非空即权威——主路由档位走
|
|
35
|
+
// primaryEffort、条目档位随链注入 fallbacks、旧键与全局档位接管让位);
|
|
36
|
+
// 未配置链时旧键(distillProvider/distillModel/reasoningEffort)走下方完全
|
|
37
|
+
// 不变的旧路径——旧存量值的语义一个比特都不动(projectDistillChain 只是
|
|
38
|
+
// llm-providers 的 UI 展示视图,不参与生效逻辑)。
|
|
39
|
+
// pinned 时链整体失效(部署锁定路由,链编辑器只读)
|
|
40
|
+
const chain = s?.distillChain?.length ? s.distillChain : [];
|
|
41
|
+
const chainMode = chain.length > 0 && !pinned;
|
|
42
|
+
const chainEffort = chainMode && chain[0].reasoningEffort ? chain[0].reasoningEffort : null;
|
|
43
|
+
// 链非空即整体接管:回退链以运行时链为准(slice(1)),**单行链 = 显式无回退**
|
|
44
|
+
// (空数组覆盖静态 cfg.llm.fallbacks——"UI 所见即所跑",审查修复的语义不对称:
|
|
45
|
+
// 此前单显式行不清静态、单空主路由行清静态,同一意图两种表达)
|
|
46
|
+
const chainFallbacks = chainMode
|
|
47
|
+
? chain.slice(1).map((e) => ({ provider: e.provider, model: e.model, reasoningEffort: e.reasoningEffort || '' }))
|
|
35
48
|
: null;
|
|
49
|
+
const override = chainMode && chain[0].provider && chain[0].model
|
|
50
|
+
? { provider: chain[0].provider, model: chain[0].model }
|
|
51
|
+
: !chain.length && s && !pinned && s.distillProvider && s.distillModel
|
|
52
|
+
? { provider: s.distillProvider, model: s.distillModel }
|
|
53
|
+
: null;
|
|
36
54
|
const b = s?.distillBudgets;
|
|
37
55
|
const budgets = b && (b.extract > 0 || b.dedup > 0 || b.l2 > 0 || b.l3 > 0)
|
|
38
56
|
? {
|
|
@@ -42,19 +60,43 @@ export function effectiveCfg(cfg, live) {
|
|
|
42
60
|
...(b.l3 > 0 ? { l3: b.l3 } : {}),
|
|
43
61
|
}
|
|
44
62
|
: null;
|
|
63
|
+
// 运行时按层路由链(#34):非空层链逐层注入 layerChainsRuntime(解析侧层内第一
|
|
64
|
+
// 优先级);pinned = 部署锁,运行时层链与运行时全局链一并失效(同 chainMode 的
|
|
65
|
+
// pinned 处理);全部层为空 = 无注入(返回引用不变路径不受影响)。层键逐个
|
|
66
|
+
// ?.length 容忍部分键缺失(旧存量 settings / 手写覆盖可能只带部分层)
|
|
67
|
+
const lc = s?.distillLayerChains;
|
|
68
|
+
const layerPick = lc && !pinned
|
|
69
|
+
? {
|
|
70
|
+
...(lc.l1?.length ? { l1: lc.l1 } : {}),
|
|
71
|
+
...(lc.l2?.length ? { l2: lc.l2 } : {}),
|
|
72
|
+
...(lc.l3?.length ? { l3: lc.l3 } : {}),
|
|
73
|
+
}
|
|
74
|
+
: null;
|
|
75
|
+
const layerChains = layerPick && Object.keys(layerPick).length ? layerPick : null;
|
|
45
76
|
const maxInput = s && s.distillMaxInputChars > 0 ? s.distillMaxInputChars : null;
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
77
|
+
// 旧档位键的全局接管(含给静态回退条目盖章):仅非链模式保留(旧存量值兼容)
|
|
78
|
+
const fallbacksTakeover = !chainMode && eff && cfg.llm?.fallbacks?.length
|
|
79
|
+
? cfg.llm.fallbacks.map((f) => ({ ...f, reasoningEffort: eff }))
|
|
80
|
+
: null;
|
|
81
|
+
// 档位注入:链模式下不走全局接管(主路由档位走 primaryEffort、条目档位在链内,
|
|
82
|
+
// 空档位条目回退的是静态全局而非运行时旧键);非链模式保持旧整体接管语义
|
|
83
|
+
const effortInject = live && !chainMode && (eff !== '' || Boolean(cfg.llm?.reasoningEffort))
|
|
84
|
+
? { reasoningEffort: eff }
|
|
85
|
+
: null;
|
|
86
|
+
if (!override && !budgets && !maxInput && !fallbacksTakeover && !chainEffort && !chainFallbacks && !effortInject && !layerChains)
|
|
49
87
|
return cfg;
|
|
50
88
|
return {
|
|
51
89
|
...cfg,
|
|
52
90
|
llm: {
|
|
53
91
|
...cfg.llm,
|
|
54
|
-
...(
|
|
92
|
+
...(effortInject ?? {}),
|
|
55
93
|
...(override ?? {}),
|
|
56
94
|
...(budgets ? { budgets } : {}),
|
|
57
95
|
...(maxInput ? { maxInputChars: maxInput } : {}),
|
|
96
|
+
...(fallbacksTakeover ? { fallbacks: fallbacksTakeover } : {}),
|
|
97
|
+
...(chainEffort ? { primaryEffort: chainEffort } : {}),
|
|
98
|
+
...(chainFallbacks ? { fallbacks: chainFallbacks } : {}),
|
|
99
|
+
...(layerChains ? { layerChainsRuntime: layerChains } : {}),
|
|
58
100
|
},
|
|
59
101
|
};
|
|
60
102
|
}
|
package/dist/settings.d.ts
CHANGED
|
@@ -5,36 +5,32 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { Context } from '@deepseek-ai/cordis';
|
|
7
7
|
import Schema from '@deepseek-ai/schemastery';
|
|
8
|
-
import { EFFORT_CHOICES } from './config.js';
|
|
9
|
-
import type { DistillBudgetLayer } from './llm.js';
|
|
10
8
|
import type { MemoryLogger } from './types.js';
|
|
11
|
-
|
|
12
|
-
export type EffortChoice
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
distillMaxInputChars: number;
|
|
37
|
-
}
|
|
9
|
+
import type { DistillChainEntry, MemoryLiveSettings } from './contract.js';
|
|
10
|
+
export type { DistillBudgets, DistillChainEntry, EffortChoice, MemoryLiveSettings } from './contract.js';
|
|
11
|
+
/**
|
|
12
|
+
* 运行时统一路由链条目:[0] = 主路由(provider/model 双空 = 跟随默认模型),
|
|
13
|
+
* [1..] = 回退链(按序降级);reasoningEffort 为该路由的档位覆盖('' = 跟随部署全局)。
|
|
14
|
+
*/
|
|
15
|
+
/** 运行时路由链上限(写入门与 UI 同限,防误粘贴巨数组撑爆 settings 存储)。 */
|
|
16
|
+
export declare const DISTILL_CHAIN_MAX = 8;
|
|
17
|
+
/**
|
|
18
|
+
* 运行时统一路由链的**展示投影**(llm-providers 的 chain.current 数据源):
|
|
19
|
+
* distillChain 非空即原样返回;为空时投影旧运行时键(distillProvider/distillModel
|
|
20
|
+
* 成对 → 单行主路由,旧档位 reasoningEffort 作为该主路由的档位——旧语义里它
|
|
21
|
+
* 作用的就是当时唯一的路由)。注意:生效逻辑(effectiveCfg)只认显式
|
|
22
|
+
* distillChain、不走本投影——旧键路径在未配链时按旧语义原样生效。
|
|
23
|
+
*/
|
|
24
|
+
export declare function projectDistillChain(s: Partial<MemoryLiveSettings> | undefined): DistillChainEntry[];
|
|
25
|
+
/**
|
|
26
|
+
* settings-set 写入门校验:返回错误文案(null = 通过)。
|
|
27
|
+
* opts.requireExplicitHead(#34 层链用):头行必须 provider+model 双显式——层键出现
|
|
28
|
+
* 即意图覆盖;"双空 = 跟随默认模型"是全局链独有语义("默认模型 + 自定义回退"的
|
|
29
|
+
* 诉求由全局 llm.fallbacks 承担),层链禁掉双空头,消除"层链头跟随哪套全局解析"的歧义。
|
|
30
|
+
*/
|
|
31
|
+
export declare function validateDistillChain(chain: unknown, opts?: {
|
|
32
|
+
requireExplicitHead?: boolean;
|
|
33
|
+
}): string | null;
|
|
38
34
|
export interface LiveSettingsHandle {
|
|
39
35
|
/** settings 服务是否可用(不可用时 UI 侧隐藏开关面板) */
|
|
40
36
|
supported: boolean;
|