dsh-plugin-om 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/License ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fane
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,113 @@
1
+ # dsh-plugin-om
2
+
3
+ 在 DSH 里应用 [Observational Memory](https://mastra.ai/research/observational-memory) 风格的上下文管理策略
4
+
5
+ ## 工作原理
6
+
7
+ 1. 在未压缩消息超过阈值后摘要
8
+ 2. 摘要会替换原始消息,并追加至现有摘要
9
+ 3. 在摘要超过阈值后,重新摘要
10
+ 4. 摘要过程中保留关键的message_id,允许模型精确recall
11
+
12
+ ### 注意
13
+
14
+ - recall 不截断,建议保留 `tool-result-pruner`
15
+ - 默认上下文压缩插件 `compaction-basic` 到达阈值后会自动摘要,不建议和此插件一起使用
16
+
17
+ ### 依赖策略
18
+
19
+ - 以type-only的方式引用第三方库
20
+ - 复用dsh宿主提供的依赖,如 cordis / dsh-tools / zod 等
21
+
22
+ ## 安装与启用
23
+
24
+ ### 说明
25
+
26
+ - `$DSH_HOME` 缺省为 `~/.dsh`
27
+ - *profile*描述了dsh进程的启动模式,官方的启动命令就是名为`web`的profile
28
+
29
+ ### 生产使用
30
+
31
+ 安装插件
32
+
33
+ ```sh
34
+ dsh plugin --profile <profile> add dsh-plugin-om
35
+ ```
36
+
37
+ 无需重启
38
+
39
+ 可以通过`dsh --profile <profile> --dump-config`审查配置是否正确
40
+
41
+ ### 开发插件
42
+
43
+ 运行`pnpm dev`,等待`dist/index.mjs`构筑完毕
44
+
45
+ 打开 `$DSH_HOME/profiles/<profile>/cordis.patch.yml` 删除里面的空数组,加入
46
+
47
+ ```yaml
48
+ - insert:
49
+ - id: dsh-plugin-om-dev
50
+ name: file:///<repo>/dist/index.mjs
51
+ ```
52
+
53
+ 可以热重载
54
+
55
+ ### 关于`compaction-basic`
56
+
57
+ dsh的"预设"分为两层,`dsh web`等同于`dsh --profile web`,调用的是"名为web的profile",进程上的预设
58
+
59
+ 而`web-profile`里定义了多个"预设agent"`preset-agent` 这里主要说的是`preset-agent`的问题
60
+
61
+ `preset-agent`配置里自带一个`compaction-basic`。由于配置注入的顺序是层级低的覆盖层级高的,所以`cordis.patch.yml`对`compaction-basic`的禁用不会生效
62
+
63
+ 解决方案:
64
+
65
+ - 定义不含`compaction-basic`的`preset-agent`
66
+ - `compaction-basic`压缩阈值是80%上下文窗口,而只需要确保OM的配置中,`thresholdRatio`+`historyMergeRatio`<0.8,理论上没到强制摘要就会被OM压缩了
67
+
68
+ ## 插件配置项
69
+
70
+ | 键 | 默认 | 含义 |
71
+ | ------------------- | ------ | -------------------------------------------- |
72
+ | `thresholdRatio` | `0.5` | 观察阈值:未压缩消息 ≥ 窗口 × 该比例触发压缩 |
73
+ | `historyMergeRatio` | `0.2` | 反思阈值:摘要 ≥ 窗口 × 该比例触发精简合并 |
74
+ | `compressMaxTokens` | `4096` | 单次摘要(观察/反思调用)生成上限 |
75
+ | `tailMessageCount` | `10` | 压缩后保留的未压缩消息条数 |
76
+
77
+ ## npm 命令
78
+
79
+ | 命令 | 作用 |
80
+ | --------------------------- | ----------------------------------------- |
81
+ | `pnpm check` | typecheck + lint + test + build |
82
+ | `pnpm typecheck` | TypeScript 类型检查 |
83
+ | `pnpm lint` / `pnpm format` | 代码检查 / 格式化 |
84
+ | `pnpm test` | vitest 单元测试 |
85
+ | `pnpm build` | |
86
+ | `pnpm dev` | 自动打包 |
87
+ | `pnpm run release` | CHANGELOG 归档 + 版本号更新 + 打 tag 推送 |
88
+
89
+ ## 调用链和文件地图
90
+
91
+ ```
92
+ cordis.patch.yml # bundle patch:dsh plugin add 后作为组合层插入插件行(id = dsh-plugin-om)
93
+ src/
94
+ ├── index.ts # 打包入口(tsdown entry),导出 name / inject / apply
95
+ │ apply(ctx, config) 三条主线(标注对应实现文件):
96
+ │ ├─ ① resolveConfig(config) ──▶ config.ts # 配置默认值合并 + 逐键校验(留空回退默认,冻结返回)
97
+ │ ├─ ② ctx.tools.register(buildRecallTool(() => ctx.get('toolResultPruner')))
98
+ │ │ └─▶ recall.ts # recall 工具:按 message_id 回看区间(超大结果由 pruner 裁剪)
99
+ │ └─ ③ 事件接线(仅主会话生效)
100
+ │ └─ ctx.on('agent/pre-step') → compress.ts # maybeCompress:两级压缩阻塞串行(先反思后观察)
101
+ │ ├─ reflectPass → summarize.ts # 摘要 ≥ 窗口 × historyMergeRatio:fork 精简合并 <om-history>
102
+ │ └─ observePass → summarize.ts # 未压缩消息 ≥ 窗口 × thresholdRatio:fork 观察日志 → 追加 + 替换
103
+ ├── constants.ts # 共享常量(PLUGIN_LABEL / HISTORY_TAG / CLAIM_EVENT)
104
+ ├── types.ts # type-only:宿主类型再导出 + 领域类型(MessageNode / MessageIndex)
105
+ ├── config.ts # 配置默认值 / 校验(缺省、null、空串回退默认值)
106
+ ├── utils.ts # 零依赖工具函数(配置校验 / 文本渲染 / 主会话判定 / 路由解析)
107
+ ├── log-index.ts # 消息索引(message_id → 消息事件;recall 消费)
108
+ ├── summarize.ts # 观察/反思 persona + 提示词 + fork 摘要子会话
109
+ ├── recall.ts # recall 工具
110
+ └── compress.ts # 两级自动压缩(测量 / 区间计算 / 中断扫描 / 对照表 / 替换 + 影子价格认领)
111
+ scripts/ # release-archive.mjs(CHANGELOG 归档)
112
+ tests/ # vitest 单元测试(52 例)
113
+ ```
@@ -0,0 +1,63 @@
1
+ import type { Agent, Context, PluginConfig, Session } from './types.ts';
2
+ /** 历史文本 token 估算:4 字符 ≈ 1 token(与宿主 dsh-token-meter 启发式一致)。 */
3
+ export declare function estimateTextTokens(text: string): number;
4
+ /** token 估算器的结构类型(仅需 estimateMessage;避免依赖完整 TokenMeter 接口)。 */
5
+ export type TokenEstimator = {
6
+ estimateMessage(message: unknown): number;
7
+ };
8
+ /**
9
+ * 未压缩消息 token 估算:表层节点合计,不含 <om-history> 摘要节点
10
+ * (观察阈值衡量对象)。
11
+ */
12
+ export declare function measureUncompressedTokens(session: Session, meter: TokenEstimator): number;
13
+ /** 定位日志中最后一次 <om-history> 压缩日志(内文 + 事件 seq);无则 undefined。 */
14
+ export declare function findLatestHistory(session: Session): {
15
+ text: string;
16
+ seq: number;
17
+ } | undefined;
18
+ /**
19
+ * 观察压缩区间:尾部保留 tailCount 条消息不压缩,区间封顶在最后一个已结束 turn 的
20
+ * 表层节点(fork seed 截断于最后一个 turn/end,当前 turn 消息不可压缩)。
21
+ */
22
+ export declare function computeCompressRange(session: Session, tailCount: number): {
23
+ start: number;
24
+ end: number;
25
+ shadowedSeqs: number[];
26
+ lastEndSeq: number;
27
+ } | undefined;
28
+ /**
29
+ * 中断标记行:范围内 turn/end 以 aborted(含 cause 类型)或 interrupted 结束的轮次
30
+ * (标记用途:让摘要 AI 理解中断原因)。
31
+ */
32
+ export declare function scanInterruptions(session: Session, fromSeq: number, toSeq: number): string[];
33
+ /**
34
+ * 提取遮蔽区间内最后一次 <om-history> 压缩日志(内文 + seq)。
35
+ * 按表层顺序(shadowedSeqs)扫描:单节点替换(反思)后摘要节点 seq 可能大于
36
+ * 被压缩消息的 seq,按 seq 区间扫描会漏(start > end)。
37
+ */
38
+ export declare function extractHistoryText(session: Session, shadowedSeqs: readonly number[]): {
39
+ text: string;
40
+ seq: number;
41
+ } | undefined;
42
+ /**
43
+ * message_id 对照表:遮蔽区间内消息事件按表层顺序产出 id 行(插件自产 user/message
44
+ * 如运行时上下文快照与 <om-history> 不入表;观察子会话据此产出正确的 message_id)。
45
+ * 按表层顺序(shadowedSeqs)扫描:与 extractHistoryText 同理,seq 区间扫描会漏。
46
+ */
47
+ export declare function buildMessageIdTable(session: Session, shadowedSeqs: readonly number[]): string[];
48
+ /**
49
+ * 反思:摘要 tokens ≥ 窗口 × historyMergeRatio 时,fork 子会话精简合并摘要,
50
+ * 替换单个 <om-history> 节点。失败不产生部分替换。
51
+ */
52
+ export declare function reflectPass(ctx: Context, agent: Agent, config: Readonly<PluginConfig>, window: number, signal?: AbortSignal): Promise<void>;
53
+ /**
54
+ * 观察:未压缩消息 tokens ≥ 窗口 × thresholdRatio 时,fork 子会话把未压缩消息压缩为
55
+ * 观察日志,追加到旧摘要并替换被压缩消息区间。失败不产生部分替换。
56
+ */
57
+ export declare function observePass(ctx: Context, agent: Agent, config: Readonly<PluginConfig>, window: number, tailCount: number, signal?: AbortSignal): Promise<void>;
58
+ /**
59
+ * 压力检查 + 两级压缩:先反思(压缩过往摘要,有必要才做),后观察(压缩新消息,
60
+ * 有必要才做)。在 pre-step 阻塞串行执行(避免压缩失败或重复压缩)。仅主会话生效。
61
+ */
62
+ export declare function maybeCompress(ctx: Context, agent: Agent, config: Readonly<PluginConfig>, signal?: AbortSignal): Promise<void>;
63
+ //# sourceMappingURL=compress.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compress.d.ts","sourceRoot":"","sources":["../src/compress.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAgB,MAAM,YAAY,CAAC;AAGtF,+DAA+D;AAC/D,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAYD,+DAA+D;AAC/D,MAAM,MAAM,cAAc,GAAG;IAAE,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAA;CAAE,CAAC;AAE3E;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,GAAG,MAAM,CAYzF;AAED,6DAA6D;AAC7D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAS7F;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,GAChB;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CA4BxF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAmB5F;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,SAAS,MAAM,EAAE,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAS3C;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CA6B/F;AA8CD;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CA+Cf;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC9B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CA8Ef;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,KAAK,EACZ,MAAM,EAAE,QAAQ,CAAC,YAAY,CAAC,EAC9B,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,IAAI,CAAC,CAyBf"}
@@ -0,0 +1,19 @@
1
+ /** 插件配置项(全部可选覆盖,未给出或留空的键用默认值)。 */
2
+ export type PluginConfig = {
3
+ /** 压力阈值比例:压力 ≥ 窗口 × 该比例时触发自动压缩。 */
4
+ thresholdRatio: number;
5
+ /** 反思阈值比例:摘要(<om-history> 内容)≥ 窗口 × 该比例时由反思子会话精简合并。 */
6
+ historyMergeRatio: number;
7
+ /** 单次摘要(合并调用)生成上限(LLM maxTokens)。 */
8
+ compressMaxTokens: number;
9
+ /** 压缩边界:其后不压缩消息数下限(正整数)。 */
10
+ tailMessageCount: number;
11
+ };
12
+ /** 默认配置(冻结对象,resolveConfig 合并的基底)。 */
13
+ export declare const DEFAULT_CONFIG: Readonly<PluginConfig>;
14
+ /**
15
+ * 解析合并配置:校验未知键与数值类型,返回冻结的完整配置。
16
+ * 允许所有配置留空——缺省 / null / 空串的键回退默认值,未给出的键亦取默认值。
17
+ */
18
+ export declare function resolveConfig(raw?: unknown): Readonly<PluginConfig>;
19
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAMA,kCAAkC;AAClC,MAAM,MAAM,YAAY,GAAG;IACzB,mCAAmC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4BAA4B;IAC5B,gBAAgB,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,sCAAsC;AACtC,eAAO,MAAM,cAAc,EAAE,QAAQ,CAAC,YAAY,CAKhD,CAAC;AA6BH;;;GAGG;AACH,wBAAgB,aAAa,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,YAAY,CAAC,CAiBnE"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 共享常量:集中定义插件级魔法字符串与事件名,避免散落各模块。
3
+ */
4
+ /** 插件标识:压缩消息 source.plugin 取值与日志前缀。 */
5
+ export declare const PLUGIN_LABEL = "dsh-plugin-om";
6
+ /** 压缩日志标签名:<om-history>...</om-history> 包裹观察/反思日志块。 */
7
+ export declare const HISTORY_TAG = "om-history";
8
+ /** 影子价格认领事件类型:token-meter 据此识别被替换(遮蔽)的表层节点。 */
9
+ export declare const CLAIM_EVENT = "compaction/prune";
10
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,uCAAuC;AACvC,eAAO,MAAM,YAAY,kBAAkB,CAAC;AAE5C,uDAAuD;AACvD,eAAO,MAAM,WAAW,eAAe,CAAC;AAExC,+CAA+C;AAC/C,eAAO,MAAM,WAAW,qBAAqB,CAAC"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * dsh-plugin-om — Observational Memory(OM)上下文压缩 + recall 检索插件。
3
+ * 不依赖特定 tool mode(native / code / both 均可运行)。
4
+ *
5
+ * 模块:
6
+ * - recall.ts recall({ start_id, end_id?, offset? }) 工具:按 message_id 回看原始会话
7
+ * - compress.ts 自动压缩(OM 观察/反思两级阈值):pre-step 阻塞串行执行——
8
+ * 反思(摘要 ≥ 窗口 × historyMergeRatio 时 fork 精简合并 <om-history>)、
9
+ * 观察(未压缩消息 ≥ 窗口 × thresholdRatio 时 fork 压缩为观察日志并追加)
10
+ *
11
+ * 约束:不引入自定义会话事件类型——压缩复用宿主已知的 'compaction/prune' 影子价格事件。
12
+ * 仅主会话生效(subagent 不压缩、recall 拒绝)。
13
+ */
14
+ import type { Context } from './types.ts';
15
+ /** 插件名(Loader 识别入口的稳定标识)。 */
16
+ export declare const name = "dsh-plugin-om";
17
+ /** 插件注入的服务依赖(tools/llm/tokenMeter/sessions),由宿主按序注入。 */
18
+ export declare const inject: string[];
19
+ /**
20
+ * 插件激活入口:注册 recall 工具,并在 agent/pre-step 阻塞触发两级自动压缩
21
+ * (先反思后观察)。仅主会话生效。
22
+ */
23
+ export declare function apply(ctx: Context, config?: unknown): void;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,6BAA6B;AAC7B,eAAO,MAAM,IAAI,kBAAkB,CAAC;AAEpC,wDAAwD;AACxD,eAAO,MAAM,MAAM,UAA6C,CAAC;AAEjE;;;GAGG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,CAuB1D"}