@max-null/dsh-allostasis 0.1.0 → 0.2.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/README.md +55 -7
- package/cordis.patch.yml +4 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +54 -0
- package/dist/drift.d.ts +7 -2
- package/dist/drift.js +8 -3
- package/dist/events.d.ts +67 -0
- package/dist/events.js +40 -0
- package/dist/index.d.ts +20 -10
- package/dist/index.js +105 -31
- package/dist/messages.d.ts +50 -0
- package/dist/messages.js +67 -0
- package/dist/name.d.ts +15 -0
- package/dist/name.js +15 -0
- package/dist/repetition.d.ts +91 -0
- package/dist/repetition.js +100 -0
- package/dist/throttle.d.ts +11 -8
- package/dist/throttle.js +10 -7
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
This plugin belongs to the **`@max-null/*` family** — a set of plugins that together form the **[SSID (思灵 · Seek Soul in Darkness)](https://github.com/Max-Null/seek-soul-in-darkness)** desktop experience.
|
|
6
6
|
|
|
7
7
|
Allostasis for the DeepSeek Harness — session self-regulation rather than monitoring.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Before each step it reads the most recent reasoning block and appends one near-end message
|
|
9
|
+
when that thinking has drifted into English (**Chinese anchoring**) or collapsed into
|
|
10
|
+
repetition (**degeneration reminder**).
|
|
11
11
|
|
|
12
12
|
## 它做什么
|
|
13
13
|
|
|
@@ -16,10 +16,27 @@ message to bring it back.
|
|
|
16
16
|
| 能力 | 状态 | 触发条件 |
|
|
17
17
|
|---|---|---|
|
|
18
18
|
| **中文锚定** | **已实现** | 上一步思考的英文功能词密度越线 |
|
|
19
|
+
| **推理退化提醒** | **已实现** | 上一步思考的重复率越线,且连续 N 步成立 |
|
|
19
20
|
| 上下文占用感知 | 计划中 | 需先定「什么情况下才出现」(持续在场会退化成背景音) |
|
|
20
|
-
| 压缩预约落盘 | 计划中 |
|
|
21
|
+
| 压缩预约落盘 | 计划中 | 待通路验证:退化样本散布在整段退化区间,「只压一小段」能否打断循环尚无证据(二期方案 §八.1) |
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
两类提醒都以 `notice` 形式注入——在**轨迹页**是折叠态就显示一行摘要的注入行,不弹窗、不打断;
|
|
24
|
+
**对话页不显示**(2026-09-29 在 SSiD dev / DSH 0.2.0-rc.1 上实测,口径见二期方案 §九)。
|
|
25
|
+
退化触发时还会 append 一条 `allostasis/degeneration` 事件,记下当时的重复率、阈值与连续步数,
|
|
26
|
+
因此「插件当时判了什么、用的什么阈值」可事后重建。
|
|
27
|
+
|
|
28
|
+
**它全程静默,所以「确认它在工作」只能靠日志。** 本插件没有任何界面元素,命中时也只在轨迹页留一行;不命中时一个字都不说——「装了没有」「阈值生效没有」「这一步为什么没提醒」三个问题原本都无从回答,只能靠改配置去试。因此它在 `src/index.ts` 的 `apply` 里留两行痕:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
[dsh-allostasis] loaded · driftThreshold=0.15 repetitionThreshold=0.5 consecutiveSteps=2
|
|
32
|
+
[dsh-allostasis] turn 8 step 11 · drift=chinese funcDensity=0.6% chars=1764
|
|
33
|
+
· repetition=normal units=29 ratio=0%
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
第一行 `info`、每个进程一次,报的是**生效阈值**(`Config` 的解析结果,不是代码里的缺省常量)。第二行 `debug`、**每一步判定一行**:两类判据的三态结论加度量。默认静默,排查时打开即可——不必为了看一眼判定结果去动阈值。其中 `units` 是切分后的单元数,**低于 12 判 `insufficient`**(样本不足不下结论),此时 `ratio` 仍会给出,只是不参与判定。
|
|
37
|
+
|
|
38
|
+
判据来源、实测数据与完整设计见 `docs/设计/2026-09-20-应变-设计方案.md`、
|
|
39
|
+
`docs/设计/2026-09-28-应变二期-退化检测与自动干预.md`。
|
|
23
40
|
|
|
24
41
|
## 为什么需要它
|
|
25
42
|
|
|
@@ -29,6 +46,10 @@ message to bring it back.
|
|
|
29
46
|
|
|
30
47
|
## 判据
|
|
31
48
|
|
|
49
|
+
两类提醒各有一个纯函数判据,都在 `src/` 下,都附实测依据。
|
|
50
|
+
|
|
51
|
+
### 语言漂移
|
|
52
|
+
|
|
32
53
|
**英文功能词密度** =(`the` / `is` / `are` / `and` / `of` / `to` / `that` 这类功能词数)/ 英文词数,**且英文词数 ≥ 50 才判定**。
|
|
33
54
|
|
|
34
55
|
实测数据(一份 10 turn / 157 step 的真实会话):中文期中位 **0.012**、英文期中位 **0.273–0.389**,阈值 **0.15** 使两群完全分离。词数门槛不可省——中文期唯一的越线点只有 26 个英文词,小样本会让密度失真。
|
|
@@ -37,9 +58,19 @@ message to bring it back.
|
|
|
37
58
|
|
|
38
59
|
阈值由 `tools/analyze-thinking-lang.mjs` 在一份真实会话上标定,该工具随包发布。
|
|
39
60
|
|
|
61
|
+
### 推理退化
|
|
62
|
+
|
|
63
|
+
**重复率** = 单条推理按换行与中英句读切分后,**出现 ≥3 次的单元占全部单元的比例**;**单元数 ≥ 12 才判定**,**连续 N 步(默认 2)越线才触发**。
|
|
64
|
+
|
|
65
|
+
实测数据(一份 19 轮 / 416 条助手消息的真实会话):正常期 **0%–35%**、退化期 **48%–92%**,阈值 **0.5** 落在隔离带中段。连续步数不可省——正常期也有单次抖动(实测峰值 35%);代价是延迟,按同一份数据回放会晚约 3 步触发。
|
|
66
|
+
|
|
67
|
+
**退化与上下文占用脱钩**:同一会话里 26.1% 占用时重复率 84%、67.9% 时 83–92%,压缩到 26.1% 并不降低重复率。所以「调低阈值」治不了它;压缩能否治它取决于力度。完整数据见 `docs/排查/2026-09-28-推理退化与上下文占用脱钩.md`。
|
|
68
|
+
|
|
69
|
+
退化触发时除提醒消息外还会 append 一条 `allostasis/degeneration` 事件,记下当时的重复率、阈值与连续步数——判定依据因此可事后重建,而不是只能按现在的脚本重算。
|
|
70
|
+
|
|
40
71
|
## 截图
|
|
41
72
|
|
|
42
|
-
|
|
73
|
+
本插件是**会话行为调节类**:不新增任何按钮、面板或设置项。它每个 step 前读取最近一条思考,判定为语言漂移或推理退化时向请求末尾追加一条提醒,效果体现在模型行为上。
|
|
43
74
|
|
|
44
75
|
> 按《SSiD 开发手册》§9 截图规范:截图须回答「装完会多出/变成什么」的**入口与面板**。
|
|
45
76
|
> 本插件无界面元素(no UI surface),故**不适用**该项要求,改以上述行为效果说明代替。
|
|
@@ -57,7 +88,24 @@ Installs as a bundle: `dsh plugin --profile <name> add @max-null/dsh-allostasis`
|
|
|
57
88
|
|
|
58
89
|
## Config
|
|
59
90
|
|
|
60
|
-
|
|
91
|
+
三个判定阈值可在 `config` 段覆盖;省略即用括号内的默认值。
|
|
92
|
+
|
|
93
|
+
| 字段 | 默认 | 含义 |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| `driftThreshold` | `0.15` | 英文功能词密度达到此值即判为漂移 |
|
|
96
|
+
| `repetitionThreshold` | `0.5` | 推理重复率阈值,取值 0–1 |
|
|
97
|
+
| `consecutiveSteps` | `2` | 连续多少步越线才触发退化提醒 |
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
- id: allostasis
|
|
101
|
+
name: '@max-null/dsh-allostasis'
|
|
102
|
+
config:
|
|
103
|
+
repetitionThreshold: 0.6
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
判据里的**小样本门槛**(`MIN_WORDS` / `MIN_UNITS` / `REPEAT_MIN_COUNT`)不是配置项——它们不是偏好,改了就是把密度与比例算飞。值域外的取值会让插件**报错中止**而不是静默回退:静默回退会让「我明明改了配置」与「插件按默认值跑」同时成立而无从察觉。
|
|
107
|
+
|
|
108
|
+
这几个阈值都是**起点值而非标定结果**——单会话样本给出的隔离带中段,标定留给数据积累(设计方案 §5.1 的三条路:显性配置项、数据积累、LLM 自调)。
|
|
61
109
|
|
|
62
110
|
## Develop
|
|
63
111
|
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# dsh-allostasis bundle patch: one insert over the profile root.
|
|
2
2
|
# `system-prompt` / `agents` / `token-meter` already live in the host (dsh-base);
|
|
3
3
|
# this adds only the allostasis plugin, which registers its own reminders.
|
|
4
|
+
#
|
|
5
|
+
# `config` 可覆盖三个判定阈值:driftThreshold / repetitionThreshold / consecutiveSteps。
|
|
6
|
+
# 字段都可省略,省略即用插件内的默认值——这里刻意不写具体值,免得默认值有两个来源。
|
|
7
|
+
# 各字段的含义、取值范围与默认值见 README 的「配置」段。
|
|
4
8
|
- insert:
|
|
5
9
|
- id: allostasis
|
|
6
10
|
name: '@max-null/dsh-allostasis'
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件的可配置面。
|
|
3
|
+
*
|
|
4
|
+
* 只暴露**判定阈值**这一类值:它们是「宁可晚一点也别误报」这条取舍的刻度,随任务类型
|
|
5
|
+
* 与模型行为漂移,属于部署间会变的选择(DSH 插件规范 "No hardcoded tunables in
|
|
6
|
+
* plugins")。判据里的小样本门槛(`MIN_WORDS` / `MIN_UNITS` / `REPEAT_MIN_COUNT`)
|
|
7
|
+
* 不在此列——它们不是偏好,改了就是把密度与比例算飞,属于判据几何的一部分。
|
|
8
|
+
*
|
|
9
|
+
* **schema 只管形式,缺省与范围在 `resolveConfig`**:两处各管一半是有意的。schema 用
|
|
10
|
+
* `.default()` 会让输出类型变成 `number | Volatile<number>`(默认值允许是动态函数),
|
|
11
|
+
* 与接口对不上,内核自己也要在那里补 `as Schema<T>` 断言;而缺省值有两个来源之后,
|
|
12
|
+
* 「我明明改了配置」与「插件按默认值跑」就能同时成立而无从察觉。缺省值就是判据模块里的
|
|
13
|
+
* 常量本身,因此不配置时行为与配置前逐字一致。
|
|
14
|
+
*
|
|
15
|
+
* @module @max-null/dsh-allostasis/config
|
|
16
|
+
*/
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
18
|
+
/**
|
|
19
|
+
* 插件配置,与同名 schemastery schema 一起由 Loader 校验。
|
|
20
|
+
*
|
|
21
|
+
* 值域外的取值在 `resolveConfig` 里**报错中止**,不静默回退。
|
|
22
|
+
*/
|
|
23
|
+
export interface Config {
|
|
24
|
+
/** 英文功能词密度达到此值即判为漂移(默认 0.15)。实测中文期中位 0.012、英文期 0.273 以上。 */
|
|
25
|
+
driftThreshold?: number;
|
|
26
|
+
/** 重复率阈值,0–1(默认 0.5)。取 1 等于事实上关闭退化提醒。 */
|
|
27
|
+
repetitionThreshold?: number;
|
|
28
|
+
/** 连续多少步越线才触发退化提醒(默认 2)。取 1 会跟着正常期的单次抖动误报。 */
|
|
29
|
+
consecutiveSteps?: number;
|
|
30
|
+
}
|
|
31
|
+
/** {@link Config} 经校验后的形态:字段齐全,可直接参与判定。 */
|
|
32
|
+
export interface ResolvedConfig {
|
|
33
|
+
/** 见 {@link Config.driftThreshold}。 */
|
|
34
|
+
readonly driftThreshold: number;
|
|
35
|
+
/** 见 {@link Config.repetitionThreshold}。 */
|
|
36
|
+
readonly repetitionThreshold: number;
|
|
37
|
+
/** 见 {@link Config.consecutiveSteps}。 */
|
|
38
|
+
readonly consecutiveSteps: number;
|
|
39
|
+
}
|
|
40
|
+
/** Loader 用于校验 `cordis.patch.yml` 里 `config` 段的 schema。 */
|
|
41
|
+
export declare const Config: Schema<Config>;
|
|
42
|
+
/**
|
|
43
|
+
* 校验配置并补齐缺省值。
|
|
44
|
+
*
|
|
45
|
+
* 范围约束不交给 schema 的 `.min()/.max()`:那样报错只会说「number」,而说出该字段的
|
|
46
|
+
* 合理区间与收到的实际值,正是 fail-loud 的全部价值。
|
|
47
|
+
* @param config - Loader 传入的配置;未配置时传 `undefined` 或 `{}`。
|
|
48
|
+
* @returns 字段齐全且已校验的配置。
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveConfig(config?: Config): ResolvedConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件的可配置面。
|
|
3
|
+
*
|
|
4
|
+
* 只暴露**判定阈值**这一类值:它们是「宁可晚一点也别误报」这条取舍的刻度,随任务类型
|
|
5
|
+
* 与模型行为漂移,属于部署间会变的选择(DSH 插件规范 "No hardcoded tunables in
|
|
6
|
+
* plugins")。判据里的小样本门槛(`MIN_WORDS` / `MIN_UNITS` / `REPEAT_MIN_COUNT`)
|
|
7
|
+
* 不在此列——它们不是偏好,改了就是把密度与比例算飞,属于判据几何的一部分。
|
|
8
|
+
*
|
|
9
|
+
* **schema 只管形式,缺省与范围在 `resolveConfig`**:两处各管一半是有意的。schema 用
|
|
10
|
+
* `.default()` 会让输出类型变成 `number | Volatile<number>`(默认值允许是动态函数),
|
|
11
|
+
* 与接口对不上,内核自己也要在那里补 `as Schema<T>` 断言;而缺省值有两个来源之后,
|
|
12
|
+
* 「我明明改了配置」与「插件按默认值跑」就能同时成立而无从察觉。缺省值就是判据模块里的
|
|
13
|
+
* 常量本身,因此不配置时行为与配置前逐字一致。
|
|
14
|
+
*
|
|
15
|
+
* @module @max-null/dsh-allostasis/config
|
|
16
|
+
*/
|
|
17
|
+
import Schema from '@deepseek-ai/schemastery';
|
|
18
|
+
import { DRIFT_THRESHOLD } from "./drift.js";
|
|
19
|
+
import { CONSECUTIVE_STEPS, REPETITION_THRESHOLD } from "./repetition.js";
|
|
20
|
+
/** Loader 用于校验 `cordis.patch.yml` 里 `config` 段的 schema。 */
|
|
21
|
+
export const Config = Schema.object({
|
|
22
|
+
driftThreshold: Schema.number(),
|
|
23
|
+
repetitionThreshold: Schema.number(),
|
|
24
|
+
consecutiveSteps: Schema.number(),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* 校验一个 0–1 的阈值字段。
|
|
28
|
+
* @param field - 字段名,用于报错文案。
|
|
29
|
+
* @param value - 字段值。
|
|
30
|
+
* @returns 校验通过的原值。
|
|
31
|
+
*/
|
|
32
|
+
function ratio(field, value) {
|
|
33
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
34
|
+
throw new Error(`dsh-allostasis: \`${field}\` must be a number between 0 and 1, got ${value}`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* 校验配置并补齐缺省值。
|
|
40
|
+
*
|
|
41
|
+
* 范围约束不交给 schema 的 `.min()/.max()`:那样报错只会说「number」,而说出该字段的
|
|
42
|
+
* 合理区间与收到的实际值,正是 fail-loud 的全部价值。
|
|
43
|
+
* @param config - Loader 传入的配置;未配置时传 `undefined` 或 `{}`。
|
|
44
|
+
* @returns 字段齐全且已校验的配置。
|
|
45
|
+
*/
|
|
46
|
+
export function resolveConfig(config = {}) {
|
|
47
|
+
const driftThreshold = ratio('driftThreshold', config.driftThreshold ?? DRIFT_THRESHOLD);
|
|
48
|
+
const repetitionThreshold = ratio('repetitionThreshold', config.repetitionThreshold ?? REPETITION_THRESHOLD);
|
|
49
|
+
const consecutiveSteps = config.consecutiveSteps ?? CONSECUTIVE_STEPS;
|
|
50
|
+
if (!Number.isInteger(consecutiveSteps) || consecutiveSteps < 1) {
|
|
51
|
+
throw new Error(`dsh-allostasis: \`consecutiveSteps\` must be an integer >= 1, got ${consecutiveSteps}`);
|
|
52
|
+
}
|
|
53
|
+
return { driftThreshold, repetitionThreshold, consecutiveSteps };
|
|
54
|
+
}
|
package/dist/drift.d.ts
CHANGED
|
@@ -38,5 +38,10 @@ export interface DriftMetrics {
|
|
|
38
38
|
export type DriftVerdict = 'drift' | 'chinese' | 'insufficient';
|
|
39
39
|
/** 统计一条思考文本的各量。空文本返回全零。 */
|
|
40
40
|
export declare function measureThinking(text: string): DriftMetrics;
|
|
41
|
-
/**
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 对一次测量下判定。
|
|
43
|
+
* @param metrics - 量化结果。
|
|
44
|
+
* @param threshold - 功能词密度阈值;缺省用 {@link DRIFT_THRESHOLD}。
|
|
45
|
+
* @returns 三态判定。
|
|
46
|
+
*/
|
|
47
|
+
export declare function verdict(metrics: DriftMetrics, threshold?: number): DriftVerdict;
|
package/dist/drift.js
CHANGED
|
@@ -40,9 +40,14 @@ export function measureThinking(text) {
|
|
|
40
40
|
funcDensity: words.length === 0 ? 0 : funcWords / words.length,
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
|
-
/**
|
|
44
|
-
|
|
43
|
+
/**
|
|
44
|
+
* 对一次测量下判定。
|
|
45
|
+
* @param metrics - 量化结果。
|
|
46
|
+
* @param threshold - 功能词密度阈值;缺省用 {@link DRIFT_THRESHOLD}。
|
|
47
|
+
* @returns 三态判定。
|
|
48
|
+
*/
|
|
49
|
+
export function verdict(metrics, threshold = DRIFT_THRESHOLD) {
|
|
45
50
|
if (metrics.words < MIN_WORDS)
|
|
46
51
|
return 'insufficient';
|
|
47
|
-
return metrics.funcDensity >=
|
|
52
|
+
return metrics.funcDensity >= threshold ? 'drift' : 'chinese';
|
|
48
53
|
}
|
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 退化触发的会话事件——「插件当时判了什么」的落盘。
|
|
3
|
+
*
|
|
4
|
+
* 判定输入本来就可重放(推理原文在日志的 `reasoning-chunks.texts`,占用与压缩点在
|
|
5
|
+
* `data.usage` / `compaction/start`),所以「如果当时阈值是 X 会怎样」能离线回答。
|
|
6
|
+
* 缺的是**插件实际用过的阈值与判定结果**:只靠重算,复盘的是「按现在的脚本判会怎样」,
|
|
7
|
+
* 不是「插件当时判了什么」,两个口径会随时间漂移。这个事件补的就是这道缝
|
|
8
|
+
* (设计方案 §4.3 选项 A)。
|
|
9
|
+
*
|
|
10
|
+
* `SessionEventMap` 是 merge-extensible 的,内核的会话 invariant 对未知事件类型直接
|
|
11
|
+
* 放行——「Merge-extensible event relations belong to their owning plugin」
|
|
12
|
+
* (`packages/core/session/src/invariant.ts:165-167`),因此这里不需要在内核侧登记。
|
|
13
|
+
*
|
|
14
|
+
* @module @max-null/dsh-allostasis/events
|
|
15
|
+
*/
|
|
16
|
+
import type { RepetitionMetrics } from './repetition.ts';
|
|
17
|
+
declare module '@deepseek-ai/dsh-session/types' {
|
|
18
|
+
interface SessionEventMap {
|
|
19
|
+
/** 一次退化触发;只记录判定,不记录干预——压缩动作是否发生由压缩事件自己回答。 */
|
|
20
|
+
'allostasis/degeneration': DegenerationEvent;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** 落盘时每个重复单元的截断长度;够复盘看出重复的是什么,又不让日志被碎片撑大。 */
|
|
24
|
+
export declare const UNIT_SAMPLE_MAX_CHARS = 80;
|
|
25
|
+
/** {@link degenerationEvent} 的输入。 */
|
|
26
|
+
export interface DegenerationTrigger {
|
|
27
|
+
/** 产出该推理的 turn。 */
|
|
28
|
+
readonly turn: number;
|
|
29
|
+
/** 产出该推理的 step。 */
|
|
30
|
+
readonly step: number;
|
|
31
|
+
/** 该步的重复度量化结果。 */
|
|
32
|
+
readonly metrics: RepetitionMetrics;
|
|
33
|
+
/** 已连续越线的步数。 */
|
|
34
|
+
readonly consecutive: number;
|
|
35
|
+
/** 判定所用的重复率阈值。 */
|
|
36
|
+
readonly threshold: number;
|
|
37
|
+
/** 触发所需的连续步数。 */
|
|
38
|
+
readonly required: number;
|
|
39
|
+
}
|
|
40
|
+
/** `allostasis/degeneration` 的 payload:一次触发的完整判定依据。 */
|
|
41
|
+
export interface DegenerationEvent {
|
|
42
|
+
/** 产出该推理的 turn。 */
|
|
43
|
+
turn: number;
|
|
44
|
+
/** 产出该推理的 step。 */
|
|
45
|
+
step: number;
|
|
46
|
+
/** 触发时的重复率。 */
|
|
47
|
+
ratio: number;
|
|
48
|
+
/** 单元总数,即重复率的样本量。 */
|
|
49
|
+
units: number;
|
|
50
|
+
/** 已连续越线的步数。 */
|
|
51
|
+
consecutive: number;
|
|
52
|
+
/** 判定所用的重复率阈值。 */
|
|
53
|
+
threshold: number;
|
|
54
|
+
/** 触发所需的连续步数。 */
|
|
55
|
+
required: number;
|
|
56
|
+
/** 最高频的重复单元,按出现次数降序,单元已截断。 */
|
|
57
|
+
top: Array<{
|
|
58
|
+
unit: string;
|
|
59
|
+
count: number;
|
|
60
|
+
}>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* 把一次触发整理成事件 payload。
|
|
64
|
+
* @param trigger - 触发时的判定依据。
|
|
65
|
+
* @returns 可直接交给 `session.append` 的 payload。
|
|
66
|
+
*/
|
|
67
|
+
export declare function degenerationEvent(trigger: DegenerationTrigger): DegenerationEvent;
|
package/dist/events.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 退化触发的会话事件——「插件当时判了什么」的落盘。
|
|
3
|
+
*
|
|
4
|
+
* 判定输入本来就可重放(推理原文在日志的 `reasoning-chunks.texts`,占用与压缩点在
|
|
5
|
+
* `data.usage` / `compaction/start`),所以「如果当时阈值是 X 会怎样」能离线回答。
|
|
6
|
+
* 缺的是**插件实际用过的阈值与判定结果**:只靠重算,复盘的是「按现在的脚本判会怎样」,
|
|
7
|
+
* 不是「插件当时判了什么」,两个口径会随时间漂移。这个事件补的就是这道缝
|
|
8
|
+
* (设计方案 §4.3 选项 A)。
|
|
9
|
+
*
|
|
10
|
+
* `SessionEventMap` 是 merge-extensible 的,内核的会话 invariant 对未知事件类型直接
|
|
11
|
+
* 放行——「Merge-extensible event relations belong to their owning plugin」
|
|
12
|
+
* (`packages/core/session/src/invariant.ts:165-167`),因此这里不需要在内核侧登记。
|
|
13
|
+
*
|
|
14
|
+
* @module @max-null/dsh-allostasis/events
|
|
15
|
+
*/
|
|
16
|
+
/** 落盘时每个重复单元的截断长度;够复盘看出重复的是什么,又不让日志被碎片撑大。 */
|
|
17
|
+
export const UNIT_SAMPLE_MAX_CHARS = 80;
|
|
18
|
+
/**
|
|
19
|
+
* 把一次触发整理成事件 payload。
|
|
20
|
+
* @param trigger - 触发时的判定依据。
|
|
21
|
+
* @returns 可直接交给 `session.append` 的 payload。
|
|
22
|
+
*/
|
|
23
|
+
export function degenerationEvent(trigger) {
|
|
24
|
+
const { metrics } = trigger;
|
|
25
|
+
return {
|
|
26
|
+
turn: trigger.turn,
|
|
27
|
+
step: trigger.step,
|
|
28
|
+
ratio: metrics.ratio,
|
|
29
|
+
units: metrics.units,
|
|
30
|
+
consecutive: trigger.consecutive,
|
|
31
|
+
threshold: trigger.threshold,
|
|
32
|
+
required: trigger.required,
|
|
33
|
+
top: metrics.top.map(entry => ({
|
|
34
|
+
unit: entry.unit.length <= UNIT_SAMPLE_MAX_CHARS
|
|
35
|
+
? entry.unit
|
|
36
|
+
: `${entry.unit.slice(0, UNIT_SAMPLE_MAX_CHARS)}…`,
|
|
37
|
+
count: entry.count,
|
|
38
|
+
})),
|
|
39
|
+
};
|
|
40
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,33 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 应变(allostasis):会话状态的自我调节。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 两期能力都落在同一个 `agent/pre-step` 上,因为它们读的是同一份输入——最近一条思考:
|
|
5
|
+
*
|
|
6
|
+
* · **一期 · 中文锚定**:判定为语言漂移(英文功能词密度越线)就追加一条中文锚定消息。
|
|
7
|
+
* · **二期 · 退化提醒**:判定为推理退化(重复率越线且连续若干步成立)就追加减速提醒,
|
|
8
|
+
* 并向会话日志 append 一条 `allostasis/degeneration` 记录当时的判定依据。
|
|
9
|
+
*
|
|
10
|
+
* **平时不出现、异常时才出现**——按需出现是它作为信号的前提。但异常持续时也不每个 step
|
|
11
|
+
* 都提醒:同一 turn 至多一次,见 `throttle.ts`。两类提醒各有独立的一份节流状态:判据无关,
|
|
12
|
+
* 共用状态会让先说的那类把另一类挡在门外。
|
|
8
13
|
*
|
|
9
14
|
* 为什么不挂到 system prompt 前缀上:那是 `dsh-chinese-thinking` 的位置,它作为基线
|
|
10
15
|
* 永远在场。而基线在长会话里会失效(固定前缀离输出最远,语言模式受近因支配)。本插件
|
|
11
|
-
*
|
|
16
|
+
* 补的是「**你正在漂移 / 正在打转**」这个纠偏信号,因此必须落在近因位置。
|
|
12
17
|
*
|
|
13
18
|
* 为什么用 `agent/pre-step` 而不是 `systemPrompt.context()`:前者直接给出 `turn` /
|
|
14
19
|
* `step`,且追加的是一条独立消息,落点比快照里的一个段更靠后。用法先例见官方
|
|
15
20
|
* `packages/context/time-context/src/index.ts:180-220`。
|
|
16
21
|
*
|
|
17
|
-
*
|
|
22
|
+
* 阈值全部走 `config.ts` 的配置面。**压缩动作按设计方案 §九 暂缓**:数据指向退化样本
|
|
23
|
+
* 散布在整段退化区间,而 `compactRegion` 只压指定区间,能否打断循环尚未验证。
|
|
24
|
+
*
|
|
25
|
+
* 设计出处:`docs/设计/2026-09-20-应变-设计方案.md`、
|
|
26
|
+
* `docs/设计/2026-09-28-应变二期-退化检测与自动干预.md`
|
|
18
27
|
* @module @max-null/dsh-allostasis
|
|
19
28
|
*/
|
|
20
29
|
import type { Context } from '@deepseek-ai/cordis';
|
|
21
30
|
import { anchorText } from './anchor.ts';
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
import { Config } from './config.ts';
|
|
32
|
+
import { name } from './name.ts';
|
|
33
|
+
export { anchorText, Config, name };
|
|
24
34
|
/** 需要 `agents` 服务来接收 `agent/pre-step` 事件。 */
|
|
25
35
|
export declare const inject: string[];
|
|
26
|
-
export { anchorText };
|
|
27
36
|
/**
|
|
28
37
|
* 注册 pre-step 监听器;监听器随 `ctx` 生命周期销毁。
|
|
29
38
|
*
|
|
30
39
|
* 用 `{ prepend: true }` 以取得 `next()` 的决策后再追加,与官方 `time-context` 一致。
|
|
31
40
|
* @param ctx - 插件上下文。
|
|
41
|
+
* @param config - `cordis.patch.yml` 里的配置段;省略时全部取判据模块的缺省值。
|
|
32
42
|
*/
|
|
33
|
-
export declare function apply(ctx: Context): void;
|
|
43
|
+
export declare function apply(ctx: Context, config?: Config): void;
|
package/dist/index.js
CHANGED
|
@@ -1,41 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 应变(allostasis):会话状态的自我调节。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 两期能力都落在同一个 `agent/pre-step` 上,因为它们读的是同一份输入——最近一条思考:
|
|
5
|
+
*
|
|
6
|
+
* · **一期 · 中文锚定**:判定为语言漂移(英文功能词密度越线)就追加一条中文锚定消息。
|
|
7
|
+
* · **二期 · 退化提醒**:判定为推理退化(重复率越线且连续若干步成立)就追加减速提醒,
|
|
8
|
+
* 并向会话日志 append 一条 `allostasis/degeneration` 记录当时的判定依据。
|
|
9
|
+
*
|
|
10
|
+
* **平时不出现、异常时才出现**——按需出现是它作为信号的前提。但异常持续时也不每个 step
|
|
11
|
+
* 都提醒:同一 turn 至多一次,见 `throttle.ts`。两类提醒各有独立的一份节流状态:判据无关,
|
|
12
|
+
* 共用状态会让先说的那类把另一类挡在门外。
|
|
8
13
|
*
|
|
9
14
|
* 为什么不挂到 system prompt 前缀上:那是 `dsh-chinese-thinking` 的位置,它作为基线
|
|
10
15
|
* 永远在场。而基线在长会话里会失效(固定前缀离输出最远,语言模式受近因支配)。本插件
|
|
11
|
-
*
|
|
16
|
+
* 补的是「**你正在漂移 / 正在打转**」这个纠偏信号,因此必须落在近因位置。
|
|
12
17
|
*
|
|
13
18
|
* 为什么用 `agent/pre-step` 而不是 `systemPrompt.context()`:前者直接给出 `turn` /
|
|
14
19
|
* `step`,且追加的是一条独立消息,落点比快照里的一个段更靠后。用法先例见官方
|
|
15
20
|
* `packages/context/time-context/src/index.ts:180-220`。
|
|
16
21
|
*
|
|
17
|
-
*
|
|
22
|
+
* 阈值全部走 `config.ts` 的配置面。**压缩动作按设计方案 §九 暂缓**:数据指向退化样本
|
|
23
|
+
* 散布在整段退化区间,而 `compactRegion` 只压指定区间,能否打断循环尚未验证。
|
|
24
|
+
*
|
|
25
|
+
* 设计出处:`docs/设计/2026-09-20-应变-设计方案.md`、
|
|
26
|
+
* `docs/设计/2026-09-28-应变二期-退化检测与自动干预.md`
|
|
18
27
|
* @module @max-null/dsh-allostasis
|
|
19
28
|
*/
|
|
20
|
-
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
21
29
|
import { anchorText } from "./anchor.js";
|
|
30
|
+
import { Config, resolveConfig } from "./config.js";
|
|
22
31
|
import { measureThinking, verdict } from "./drift.js";
|
|
32
|
+
import { degenerationEvent } from "./events.js";
|
|
33
|
+
import { degenerationText, pluginNotice } from "./messages.js";
|
|
34
|
+
import { name } from "./name.js";
|
|
35
|
+
import { measureRepetition, repetitionVerdict, trackLoop } from "./repetition.js";
|
|
23
36
|
import { latestThinking } from "./thinking.js";
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
export const name = 'dsh-allostasis';
|
|
37
|
+
import { admitPerTurn } from "./throttle.js";
|
|
38
|
+
export { anchorText, Config, name };
|
|
27
39
|
/** 需要 `agents` 服务来接收 `agent/pre-step` 事件。 */
|
|
28
40
|
export const inject = ['agents'];
|
|
29
|
-
|
|
41
|
+
/**
|
|
42
|
+
* 组装一期的语言漂移提醒。
|
|
43
|
+
* @param session - 产出该思考的会话,用作节流状态的键。
|
|
44
|
+
* @param sample - 最近一条思考。
|
|
45
|
+
* @param resolved - 已校验的配置。
|
|
46
|
+
* @param throttles - 漂移提醒的节流状态表。
|
|
47
|
+
* @returns 应当追加的消息;本步不提醒时返回 `undefined`。
|
|
48
|
+
*/
|
|
49
|
+
function driftMessage(session, sample, resolved, throttles) {
|
|
50
|
+
const metrics = measureThinking(sample.text);
|
|
51
|
+
if (verdict(metrics, resolved.driftThreshold) !== 'drift')
|
|
52
|
+
return undefined;
|
|
53
|
+
const advanced = admitPerTurn(throttles.get(session), sample.turn);
|
|
54
|
+
if (advanced === undefined)
|
|
55
|
+
return undefined;
|
|
56
|
+
throttles.set(session, advanced);
|
|
57
|
+
return pluginNotice(anchorText(sample.turn, sample.step, metrics, advanced.count), `语言漂移 · turn ${sample.turn} · 第 ${advanced.count} 次`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 组装二期的推理退化提醒,并落下判定依据。
|
|
61
|
+
*
|
|
62
|
+
* 追踪状态**每步都推进**,包括被节流挡住的那几步:计数描述的是退化本身持续了多久,
|
|
63
|
+
* 与「这一步有没有说出口」无关。
|
|
64
|
+
* @param session - 产出该思考的会话。
|
|
65
|
+
* @param sample - 最近一条思考。
|
|
66
|
+
* @param resolved - 已校验的配置。
|
|
67
|
+
* @param trackers - 连续越线的追踪状态表。
|
|
68
|
+
* @param throttles - 退化提醒的节流状态表。
|
|
69
|
+
* @returns 应当追加的消息;本步不提醒时返回 `undefined`。
|
|
70
|
+
*/
|
|
71
|
+
function loopMessage(session, sample, resolved, trackers, throttles) {
|
|
72
|
+
const metrics = measureRepetition(sample.text);
|
|
73
|
+
const stepVerdict = repetitionVerdict(metrics, resolved.repetitionThreshold);
|
|
74
|
+
const tracked = trackLoop(trackers.get(session), sample.turn, stepVerdict, resolved.consecutiveSteps);
|
|
75
|
+
trackers.set(session, tracked.state);
|
|
76
|
+
if (!tracked.fire)
|
|
77
|
+
return undefined;
|
|
78
|
+
const advanced = admitPerTurn(throttles.get(session), sample.turn);
|
|
79
|
+
if (advanced === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
throttles.set(session, advanced);
|
|
82
|
+
session.append('allostasis/degeneration', degenerationEvent({
|
|
83
|
+
turn: sample.turn,
|
|
84
|
+
step: sample.step,
|
|
85
|
+
metrics,
|
|
86
|
+
consecutive: tracked.state.consecutive,
|
|
87
|
+
threshold: resolved.repetitionThreshold,
|
|
88
|
+
required: resolved.consecutiveSteps,
|
|
89
|
+
}));
|
|
90
|
+
return pluginNotice(degenerationText(sample.turn, sample.step, metrics, advanced.count), `推理退化 · turn ${sample.turn} · 重复率 ${(metrics.ratio * 100).toFixed(0)}%`);
|
|
91
|
+
}
|
|
30
92
|
/**
|
|
31
93
|
* 注册 pre-step 监听器;监听器随 `ctx` 生命周期销毁。
|
|
32
94
|
*
|
|
33
95
|
* 用 `{ prepend: true }` 以取得 `next()` 的决策后再追加,与官方 `time-context` 一致。
|
|
34
96
|
* @param ctx - 插件上下文。
|
|
97
|
+
* @param config - `cordis.patch.yml` 里的配置段;省略时全部取判据模块的缺省值。
|
|
35
98
|
*/
|
|
36
|
-
export function apply(ctx) {
|
|
37
|
-
|
|
38
|
-
|
|
99
|
+
export function apply(ctx, config = {}) {
|
|
100
|
+
const resolved = resolveConfig(config);
|
|
101
|
+
// 留痕:本插件没有任何界面元素,命中时也只在轨迹页留一行摘要,因此「装了没有、
|
|
102
|
+
// 生效阈值是多少、每步判成了什么」必须能从日志直接读到——否则装上了也无从判断,
|
|
103
|
+
// 更无从测试(2026-09-29:确认不了它是否加载)。加载行用 info(每个进程一次);
|
|
104
|
+
// 判定行走 debug,默认静默、排查时打开即可,不必为了看一眼判定去改阈值试。
|
|
105
|
+
console.info(`[${name}] loaded · driftThreshold=${resolved.driftThreshold}`
|
|
106
|
+
+ ` repetitionThreshold=${resolved.repetitionThreshold}`
|
|
107
|
+
+ ` consecutiveSteps=${resolved.consecutiveSteps}`);
|
|
108
|
+
/** 每个会话各一份状态;用 WeakMap 以免会话销毁后残留。 */
|
|
109
|
+
const anchorThrottles = new WeakMap();
|
|
110
|
+
const loopThrottles = new WeakMap();
|
|
111
|
+
const loopTrackers = new WeakMap();
|
|
39
112
|
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
40
113
|
const decision = await next();
|
|
41
114
|
if (decision.kind === 'reject' || signal.aborted)
|
|
@@ -43,23 +116,24 @@ export function apply(ctx) {
|
|
|
43
116
|
const sample = latestThinking(agent.session);
|
|
44
117
|
if (sample === undefined)
|
|
45
118
|
return decision;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
119
|
+
// 诊断行与下面的判定各算一次度量:两者都是纯字符串统计,重复计算的代价远低于
|
|
120
|
+
// 让 metrics 在三个函数之间穿梭带来的耦合。
|
|
121
|
+
const dMetrics = measureThinking(sample.text);
|
|
122
|
+
const rMetrics = measureRepetition(sample.text);
|
|
123
|
+
console.debug(`[${name}] turn ${sample.turn} step ${sample.step}`
|
|
124
|
+
+ ` · drift=${verdict(dMetrics, resolved.driftThreshold)}`
|
|
125
|
+
+ ` funcDensity=${(dMetrics.funcDensity * 100).toFixed(1)}% chars=${dMetrics.chars}`
|
|
126
|
+
+ ` · repetition=${repetitionVerdict(rMetrics, resolved.repetitionThreshold)}`
|
|
127
|
+
+ ` units=${rMetrics.units} ratio=${(rMetrics.ratio * 100).toFixed(0)}%`);
|
|
128
|
+
const appended = [];
|
|
129
|
+
const drift = driftMessage(agent.session, sample, resolved, anchorThrottles);
|
|
130
|
+
if (drift !== undefined)
|
|
131
|
+
appended.push(drift);
|
|
132
|
+
const loop = loopMessage(agent.session, sample, resolved, loopTrackers, loopThrottles);
|
|
133
|
+
if (loop !== undefined)
|
|
134
|
+
appended.push(loop);
|
|
135
|
+
if (appended.length === 0)
|
|
51
136
|
return decision;
|
|
52
|
-
|
|
53
|
-
const text = anchorText(sample.turn, sample.step, metrics, advanced.count);
|
|
54
|
-
return {
|
|
55
|
-
...decision,
|
|
56
|
-
messages: [
|
|
57
|
-
...decision.messages,
|
|
58
|
-
createUserMessage({
|
|
59
|
-
content: [{ type: 'text', text }],
|
|
60
|
-
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
|
|
61
|
-
}),
|
|
62
|
-
],
|
|
63
|
-
};
|
|
137
|
+
return { ...decision, messages: [...decision.messages, ...appended] };
|
|
64
138
|
}, { prepend: true });
|
|
65
139
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本插件产生的消息:source 登记与正文组装。
|
|
3
|
+
*
|
|
4
|
+
* 单独成文件与 `anchor.ts` 同因——入口要导入 `@deepseek-ai/dsh-llm` 的运行时值,
|
|
5
|
+
* 而测试环境只装了 npm 上的部分 DSH 包。
|
|
6
|
+
*
|
|
7
|
+
* **source 用 `notice` 而不是 `snapshot`**:`snapshot` 的语义是「同一生产者的后一份
|
|
8
|
+
* 取代前一份」,而每次提醒都是独立的即时信号,谁也不取代谁——那是 `notice` 的定义
|
|
9
|
+
* (`llm/llm/src/message.ts:60-67`)。两者的差别不只是措辞:轨迹视图按 form 决定
|
|
10
|
+
* 呈现方式,snapshot 会被当作可折叠的上下文块,notice 是一行「刚发生了什么」。
|
|
11
|
+
*
|
|
12
|
+
* **kind 走声明合并而不是类型断言**:`MessageSourceMap` 是可合并扩展的,官方明确
|
|
13
|
+
* 「each producer declares its own `kind` in its own module; there is no shared
|
|
14
|
+
* catch-all `plugin` kind」(同文件 L303-307),官方插件如 `repeat-tool-reminder`
|
|
15
|
+
* 正是这么登记的。`plugin:<名>` 是 v4 会话格式对第三方生产者的规范形式
|
|
16
|
+
* (`session-format-v3-to-v4/src/sources.ts:64` 的 `return \`plugin:${plugin}\``)。
|
|
17
|
+
* @module @max-null/dsh-allostasis/messages
|
|
18
|
+
*/
|
|
19
|
+
import type { ContextFormed } from '@deepseek-ai/dsh-llm';
|
|
20
|
+
import type { UserMessage } from '@deepseek-ai/dsh-session';
|
|
21
|
+
import { SOURCE_KIND } from './name.ts';
|
|
22
|
+
import type { RepetitionMetrics } from './repetition.ts';
|
|
23
|
+
declare module '@deepseek-ai/dsh-llm' {
|
|
24
|
+
interface MessageSourceMap {
|
|
25
|
+
/** 应变注入的即时提醒:语言漂移锚定与推理退化提醒共用这一个生产者身份。 */
|
|
26
|
+
'plugin:dsh-allostasis': {
|
|
27
|
+
kind: typeof SOURCE_KIND;
|
|
28
|
+
} & ContextFormed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 把一段提醒正文包成一条 `notice` 形式的消息。
|
|
33
|
+
* @param text - 模型读到的正文。
|
|
34
|
+
* @param summary - 客户端一行叙述;超长会被 `boundContextSummary` 截断。
|
|
35
|
+
* @returns 可追加到 pre-step 决策上的用户消息。
|
|
36
|
+
*/
|
|
37
|
+
export declare function pluginNotice(text: string, summary: string): UserMessage;
|
|
38
|
+
/**
|
|
39
|
+
* 组装推理退化提醒。
|
|
40
|
+
*
|
|
41
|
+
* 刻意点明「重复不等于想得更细」:退化期的输出读起来像在认真铺陈,若不说明,模型可能
|
|
42
|
+
* 把重复当成详尽而继续加码。给的两条出路是具体的——给结论,或换一个与前面不同的动作,
|
|
43
|
+
* 而不是「请继续努力」这类没有落点的督促。
|
|
44
|
+
* @param turn - 产出该思考的 turn。
|
|
45
|
+
* @param step - 产出该思考的 step。
|
|
46
|
+
* @param metrics - 那一步思考的重复度量化结果。
|
|
47
|
+
* @param reminder - 这是本会话第几次退化提醒;默认 1。
|
|
48
|
+
* @returns 一条退化提醒消息的正文。
|
|
49
|
+
*/
|
|
50
|
+
export declare function degenerationText(turn: number, step: number, metrics: RepetitionMetrics, reminder?: number): string;
|
package/dist/messages.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本插件产生的消息:source 登记与正文组装。
|
|
3
|
+
*
|
|
4
|
+
* 单独成文件与 `anchor.ts` 同因——入口要导入 `@deepseek-ai/dsh-llm` 的运行时值,
|
|
5
|
+
* 而测试环境只装了 npm 上的部分 DSH 包。
|
|
6
|
+
*
|
|
7
|
+
* **source 用 `notice` 而不是 `snapshot`**:`snapshot` 的语义是「同一生产者的后一份
|
|
8
|
+
* 取代前一份」,而每次提醒都是独立的即时信号,谁也不取代谁——那是 `notice` 的定义
|
|
9
|
+
* (`llm/llm/src/message.ts:60-67`)。两者的差别不只是措辞:轨迹视图按 form 决定
|
|
10
|
+
* 呈现方式,snapshot 会被当作可折叠的上下文块,notice 是一行「刚发生了什么」。
|
|
11
|
+
*
|
|
12
|
+
* **kind 走声明合并而不是类型断言**:`MessageSourceMap` 是可合并扩展的,官方明确
|
|
13
|
+
* 「each producer declares its own `kind` in its own module; there is no shared
|
|
14
|
+
* catch-all `plugin` kind」(同文件 L303-307),官方插件如 `repeat-tool-reminder`
|
|
15
|
+
* 正是这么登记的。`plugin:<名>` 是 v4 会话格式对第三方生产者的规范形式
|
|
16
|
+
* (`session-format-v3-to-v4/src/sources.ts:64` 的 `return \`plugin:${plugin}\``)。
|
|
17
|
+
* @module @max-null/dsh-allostasis/messages
|
|
18
|
+
*/
|
|
19
|
+
import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
20
|
+
import { SOURCE_KIND } from "./name.js";
|
|
21
|
+
/** 提醒文本里每个重复单元的截断长度;比落盘的 `UNIT_SAMPLE_MAX_CHARS` 更短,因为它要读起来顺。 */
|
|
22
|
+
const TOP_UNIT_MAX_CHARS = 40;
|
|
23
|
+
/** 提醒文本里最多列举几个高频单元。 */
|
|
24
|
+
const TOP_UNITS_SHOWN = 3;
|
|
25
|
+
/**
|
|
26
|
+
* 把一段提醒正文包成一条 `notice` 形式的消息。
|
|
27
|
+
* @param text - 模型读到的正文。
|
|
28
|
+
* @param summary - 客户端一行叙述;超长会被 `boundContextSummary` 截断。
|
|
29
|
+
* @returns 可追加到 pre-step 决策上的用户消息。
|
|
30
|
+
*/
|
|
31
|
+
export function pluginNotice(text, summary) {
|
|
32
|
+
return createUserMessage({
|
|
33
|
+
content: [{ type: 'text', text }],
|
|
34
|
+
source: {
|
|
35
|
+
kind: SOURCE_KIND,
|
|
36
|
+
form: 'notice',
|
|
37
|
+
summary: boundContextSummary(summary),
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 组装推理退化提醒。
|
|
43
|
+
*
|
|
44
|
+
* 刻意点明「重复不等于想得更细」:退化期的输出读起来像在认真铺陈,若不说明,模型可能
|
|
45
|
+
* 把重复当成详尽而继续加码。给的两条出路是具体的——给结论,或换一个与前面不同的动作,
|
|
46
|
+
* 而不是「请继续努力」这类没有落点的督促。
|
|
47
|
+
* @param turn - 产出该思考的 turn。
|
|
48
|
+
* @param step - 产出该思考的 step。
|
|
49
|
+
* @param metrics - 那一步思考的重复度量化结果。
|
|
50
|
+
* @param reminder - 这是本会话第几次退化提醒;默认 1。
|
|
51
|
+
* @returns 一条退化提醒消息的正文。
|
|
52
|
+
*/
|
|
53
|
+
export function degenerationText(turn, step, metrics, reminder = 1) {
|
|
54
|
+
const head = reminder > 1
|
|
55
|
+
? `⚠️ 推理退化提醒(应变,第 ${reminder} 次):你仍然在重复自己——上一步`
|
|
56
|
+
: '⚠️ 推理退化提醒(应变):你上一步';
|
|
57
|
+
const where = `(turn ${turn} step ${step})的思考在重复自己——重复率 `
|
|
58
|
+
+ `${(metrics.ratio * 100).toFixed(0)}%(${metrics.units} 个片段里有 `
|
|
59
|
+
+ `${metrics.repeated} 个出现 3 次以上)`;
|
|
60
|
+
const top = metrics.top.slice(0, TOP_UNITS_SHOWN)
|
|
61
|
+
.map(entry => `「${entry.unit.length <= TOP_UNIT_MAX_CHARS ? entry.unit : `${entry.unit.slice(0, TOP_UNIT_MAX_CHARS)}…`}」×${entry.count}`)
|
|
62
|
+
.join('、');
|
|
63
|
+
return `${head}${where},最高频的是 ${top}。`
|
|
64
|
+
+ '重复不等于想得更细,它是原地打转:这些片段没有带来新信息。'
|
|
65
|
+
+ '现在检查手上已有的信息够不够完成任务——够就直接给结论,'
|
|
66
|
+
+ '不够就换一个与前面不同的动作去取,而不是把同一句话再写一遍。';
|
|
67
|
+
}
|
package/dist/name.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis 插件名。单独成模块是为了让 `messages.ts` 与入口共用同一个字面量——
|
|
3
|
+
* 它同时是插件身份、注入消息的 `source.plugin` 与轨迹里的生产者标签,两处漂移的代价
|
|
4
|
+
* 是日志里出现两个看似无关的生产者。
|
|
5
|
+
* @module @max-null/dsh-allostasis/name
|
|
6
|
+
*/
|
|
7
|
+
/** Cordis 插件名,同时用作注入消息 source 的 kind 与 section 名。 */
|
|
8
|
+
export declare const name = "dsh-allostasis";
|
|
9
|
+
/**
|
|
10
|
+
* 注入消息 source 的 `kind`:v4 会话格式对第三方生产者的规范形式。
|
|
11
|
+
*
|
|
12
|
+
* `as const` 是必需的——`MessageSourceMap` 的成员键必须是字面量类型,而模板字符串
|
|
13
|
+
* 默认推断为 `string`。`messages.ts` 的声明合并键与这里必须是同一个值。
|
|
14
|
+
*/
|
|
15
|
+
export declare const SOURCE_KIND: "plugin:dsh-allostasis";
|
package/dist/name.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cordis 插件名。单独成模块是为了让 `messages.ts` 与入口共用同一个字面量——
|
|
3
|
+
* 它同时是插件身份、注入消息的 `source.plugin` 与轨迹里的生产者标签,两处漂移的代价
|
|
4
|
+
* 是日志里出现两个看似无关的生产者。
|
|
5
|
+
* @module @max-null/dsh-allostasis/name
|
|
6
|
+
*/
|
|
7
|
+
/** Cordis 插件名,同时用作注入消息 source 的 kind 与 section 名。 */
|
|
8
|
+
export const name = 'dsh-allostasis';
|
|
9
|
+
/**
|
|
10
|
+
* 注入消息 source 的 `kind`:v4 会话格式对第三方生产者的规范形式。
|
|
11
|
+
*
|
|
12
|
+
* `as const` 是必需的——`MessageSourceMap` 的成员键必须是字面量类型,而模板字符串
|
|
13
|
+
* 默认推断为 `string`。`messages.ts` 的声明合并键与这里必须是同一个值。
|
|
14
|
+
*/
|
|
15
|
+
export const SOURCE_KIND = `plugin:${name}`;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 思考重复度判定(退化检测的判据)。
|
|
3
|
+
*
|
|
4
|
+
* 判据 = 单条推理按换行与中英句读切分后,**出现 ≥3 次的单元占全部单元的比例**,
|
|
5
|
+
* 并要求连续若干步越线才触发。
|
|
6
|
+
*
|
|
7
|
+
* 实测依据(2026-09-28,会话 `session-fabc21b2`,19 轮 416 条助手消息):
|
|
8
|
+
* 正常期重复率 0%–35%、退化期 48%–92%,隔离带很宽。阈值取 50% 落在这份单会话样本
|
|
9
|
+
* 两端的中点附近,**不是标定结果**——它是第一期可用的起点,标定留给数据积累
|
|
10
|
+
* (设计方案 §4.2 与 §五)。
|
|
11
|
+
*
|
|
12
|
+
* 连续 N 步是必需的:正常期也会出现单次抖动(实测峰值 35%),只按单步触发会让误报
|
|
13
|
+
* 跟着抖动走。代价是延迟——按同一份数据回放,触发会落在 t11/s2 而不是 t10/s44,
|
|
14
|
+
* 晚约 3 步(中间夹了一个 48%,低于阈值、计数归零)。
|
|
15
|
+
*
|
|
16
|
+
* 为什么不用推理长度当辅助判据:退化期的单条推理并不特别长(实测 1,000–2,600
|
|
17
|
+
* 字符),长度不区分两群;重复率本身已经把「这段内容有多少信息」量化了。
|
|
18
|
+
*
|
|
19
|
+
* 设计出处:`docs/设计/2026-09-28-应变二期-退化检测与自动干预.md` §四
|
|
20
|
+
* @module @max-null/dsh-allostasis/repetition
|
|
21
|
+
*/
|
|
22
|
+
/** 单元重复达到此次数才计入「重复单元」。 */
|
|
23
|
+
export declare const REPEAT_MIN_COUNT = 3;
|
|
24
|
+
/** 判定所需的最少单元数;低于此值只报数不判定,避免小样本把比例算飞。 */
|
|
25
|
+
export declare const MIN_UNITS = 12;
|
|
26
|
+
/** 重复率判定阈值——起点值,非标定值。 */
|
|
27
|
+
export declare const REPETITION_THRESHOLD = 0.5;
|
|
28
|
+
/** 触发所需的连续越线步数。 */
|
|
29
|
+
export declare const CONSECUTIVE_STEPS = 2;
|
|
30
|
+
/** 一条思考的重复度量化结果。 */
|
|
31
|
+
export interface RepetitionMetrics {
|
|
32
|
+
/** 切分后的单元总数(去空白后)。 */
|
|
33
|
+
units: number;
|
|
34
|
+
/** 重复单元的**总出现次数**(同一单元出现 5 次计 5,不是计 1)。 */
|
|
35
|
+
repeated: number;
|
|
36
|
+
/** 重复率——判据本体。空文本与样本不足时仍给出可算的实数,判定另看 `repetitionVerdict`。 */
|
|
37
|
+
ratio: number;
|
|
38
|
+
/** 最高频的若干单元,按出现次数降序;供提醒文本直接引用。 */
|
|
39
|
+
top: Array<{
|
|
40
|
+
unit: string;
|
|
41
|
+
count: number;
|
|
42
|
+
}>;
|
|
43
|
+
}
|
|
44
|
+
/** 三态判定;单元数不足时不下结论。 */
|
|
45
|
+
export type RepetitionVerdict = 'loop' | 'normal' | 'insufficient';
|
|
46
|
+
/**
|
|
47
|
+
* 统计一条思考文本的重复度。空文本返回全零。
|
|
48
|
+
*
|
|
49
|
+
* 单元按 `UNIT_SEPARATOR` 切分并去掉空白,因此纯空行的段落不参与统计。
|
|
50
|
+
* `top` 只收达到 `REPEAT_MIN_COUNT` 的单元,最多 5 条,同次数按单元字典序稳定排序。
|
|
51
|
+
* @param text - 推理原文。
|
|
52
|
+
* @returns 量化结果。
|
|
53
|
+
*/
|
|
54
|
+
export declare function measureRepetition(text: string): RepetitionMetrics;
|
|
55
|
+
/**
|
|
56
|
+
* 对一次测量下判定。
|
|
57
|
+
*
|
|
58
|
+
* 单元数不足 `MIN_UNITS` 时返回 `insufficient`——**不下结论**。调用方据此决定该步
|
|
59
|
+
* 既不算越线也不算清白(见 `trackLoop`)。
|
|
60
|
+
* @param metrics - 量化结果。
|
|
61
|
+
* @param threshold - 重复率阈值;缺省用 {@link REPETITION_THRESHOLD}。
|
|
62
|
+
* @returns 三态判定。
|
|
63
|
+
*/
|
|
64
|
+
export declare function repetitionVerdict(metrics: RepetitionMetrics, threshold?: number): RepetitionVerdict;
|
|
65
|
+
/** 连续越线的追踪状态。不可变——每次推进都返回一份新状态。 */
|
|
66
|
+
export interface LoopTrackerState {
|
|
67
|
+
/** 当前已连续越线的步数。 */
|
|
68
|
+
consecutive: number;
|
|
69
|
+
/** 最近一次判定的 turn,用于识别「换了一轮」。 */
|
|
70
|
+
lastTurn: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* 推进连续越线计数,并判定本次是否触发。
|
|
74
|
+
*
|
|
75
|
+
* **状态不跨 turn 延续**:新一轮的第一条推理重新起算。turn 是用户可感知的边界,
|
|
76
|
+
* 而且实测里退化在同一个 turn 内就已连成片(t10 的 44%–64% 全在一轮内),跨轮累加
|
|
77
|
+
* 只会让触发更晚。
|
|
78
|
+
*
|
|
79
|
+
* `insufficient` 的样本**保持计数不变**:它既不是越线也不是清白,算作清零会让
|
|
80
|
+
* 「推理偶尔写得很短」反复推迟触发。
|
|
81
|
+
*
|
|
82
|
+
* @param state - 上一次的状态;首次调用传 `undefined`。
|
|
83
|
+
* @param turn - 产出该思考的 turn。
|
|
84
|
+
* @param verdict - 该步的判定。
|
|
85
|
+
* @param required - 触发所需的连续越线步数;缺省用 {@link CONSECUTIVE_STEPS}。
|
|
86
|
+
* @returns `state` 为推进后的新状态;`fire` 为本次是否达到触发条件。
|
|
87
|
+
*/
|
|
88
|
+
export declare function trackLoop(state: LoopTrackerState | undefined, turn: number, verdict: RepetitionVerdict, required?: number): {
|
|
89
|
+
state: LoopTrackerState;
|
|
90
|
+
fire: boolean;
|
|
91
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 思考重复度判定(退化检测的判据)。
|
|
3
|
+
*
|
|
4
|
+
* 判据 = 单条推理按换行与中英句读切分后,**出现 ≥3 次的单元占全部单元的比例**,
|
|
5
|
+
* 并要求连续若干步越线才触发。
|
|
6
|
+
*
|
|
7
|
+
* 实测依据(2026-09-28,会话 `session-fabc21b2`,19 轮 416 条助手消息):
|
|
8
|
+
* 正常期重复率 0%–35%、退化期 48%–92%,隔离带很宽。阈值取 50% 落在这份单会话样本
|
|
9
|
+
* 两端的中点附近,**不是标定结果**——它是第一期可用的起点,标定留给数据积累
|
|
10
|
+
* (设计方案 §4.2 与 §五)。
|
|
11
|
+
*
|
|
12
|
+
* 连续 N 步是必需的:正常期也会出现单次抖动(实测峰值 35%),只按单步触发会让误报
|
|
13
|
+
* 跟着抖动走。代价是延迟——按同一份数据回放,触发会落在 t11/s2 而不是 t10/s44,
|
|
14
|
+
* 晚约 3 步(中间夹了一个 48%,低于阈值、计数归零)。
|
|
15
|
+
*
|
|
16
|
+
* 为什么不用推理长度当辅助判据:退化期的单条推理并不特别长(实测 1,000–2,600
|
|
17
|
+
* 字符),长度不区分两群;重复率本身已经把「这段内容有多少信息」量化了。
|
|
18
|
+
*
|
|
19
|
+
* 设计出处:`docs/设计/2026-09-28-应变二期-退化检测与自动干预.md` §四
|
|
20
|
+
* @module @max-null/dsh-allostasis/repetition
|
|
21
|
+
*/
|
|
22
|
+
/** 单元重复达到此次数才计入「重复单元」。 */
|
|
23
|
+
export const REPEAT_MIN_COUNT = 3;
|
|
24
|
+
/** 判定所需的最少单元数;低于此值只报数不判定,避免小样本把比例算飞。 */
|
|
25
|
+
export const MIN_UNITS = 12;
|
|
26
|
+
/** 重复率判定阈值——起点值,非标定值。 */
|
|
27
|
+
export const REPETITION_THRESHOLD = 0.5;
|
|
28
|
+
/** 触发所需的连续越线步数。 */
|
|
29
|
+
export const CONSECUTIVE_STEPS = 2;
|
|
30
|
+
/** 切分单元用的分隔符:换行与中英句读。 */
|
|
31
|
+
const UNIT_SEPARATOR = /[\n。!?]/;
|
|
32
|
+
/**
|
|
33
|
+
* 统计一条思考文本的重复度。空文本返回全零。
|
|
34
|
+
*
|
|
35
|
+
* 单元按 `UNIT_SEPARATOR` 切分并去掉空白,因此纯空行的段落不参与统计。
|
|
36
|
+
* `top` 只收达到 `REPEAT_MIN_COUNT` 的单元,最多 5 条,同次数按单元字典序稳定排序。
|
|
37
|
+
* @param text - 推理原文。
|
|
38
|
+
* @returns 量化结果。
|
|
39
|
+
*/
|
|
40
|
+
export function measureRepetition(text) {
|
|
41
|
+
const units = text.split(UNIT_SEPARATOR)
|
|
42
|
+
.map(part => part.trim())
|
|
43
|
+
.filter(part => part !== '');
|
|
44
|
+
const counts = new Map();
|
|
45
|
+
for (const unit of units)
|
|
46
|
+
counts.set(unit, (counts.get(unit) ?? 0) + 1);
|
|
47
|
+
let repeated = 0;
|
|
48
|
+
const frequent = [];
|
|
49
|
+
for (const [unit, count] of counts) {
|
|
50
|
+
if (count < REPEAT_MIN_COUNT)
|
|
51
|
+
continue;
|
|
52
|
+
repeated += count;
|
|
53
|
+
frequent.push({ unit, count });
|
|
54
|
+
}
|
|
55
|
+
frequent.sort((a, b) => b.count - a.count || a.unit.localeCompare(b.unit));
|
|
56
|
+
return {
|
|
57
|
+
units: units.length,
|
|
58
|
+
repeated,
|
|
59
|
+
ratio: units.length === 0 ? 0 : repeated / units.length,
|
|
60
|
+
top: frequent.slice(0, 5),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 对一次测量下判定。
|
|
65
|
+
*
|
|
66
|
+
* 单元数不足 `MIN_UNITS` 时返回 `insufficient`——**不下结论**。调用方据此决定该步
|
|
67
|
+
* 既不算越线也不算清白(见 `trackLoop`)。
|
|
68
|
+
* @param metrics - 量化结果。
|
|
69
|
+
* @param threshold - 重复率阈值;缺省用 {@link REPETITION_THRESHOLD}。
|
|
70
|
+
* @returns 三态判定。
|
|
71
|
+
*/
|
|
72
|
+
export function repetitionVerdict(metrics, threshold = REPETITION_THRESHOLD) {
|
|
73
|
+
if (metrics.units < MIN_UNITS)
|
|
74
|
+
return 'insufficient';
|
|
75
|
+
return metrics.ratio >= threshold ? 'loop' : 'normal';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 推进连续越线计数,并判定本次是否触发。
|
|
79
|
+
*
|
|
80
|
+
* **状态不跨 turn 延续**:新一轮的第一条推理重新起算。turn 是用户可感知的边界,
|
|
81
|
+
* 而且实测里退化在同一个 turn 内就已连成片(t10 的 44%–64% 全在一轮内),跨轮累加
|
|
82
|
+
* 只会让触发更晚。
|
|
83
|
+
*
|
|
84
|
+
* `insufficient` 的样本**保持计数不变**:它既不是越线也不是清白,算作清零会让
|
|
85
|
+
* 「推理偶尔写得很短」反复推迟触发。
|
|
86
|
+
*
|
|
87
|
+
* @param state - 上一次的状态;首次调用传 `undefined`。
|
|
88
|
+
* @param turn - 产出该思考的 turn。
|
|
89
|
+
* @param verdict - 该步的判定。
|
|
90
|
+
* @param required - 触发所需的连续越线步数;缺省用 {@link CONSECUTIVE_STEPS}。
|
|
91
|
+
* @returns `state` 为推进后的新状态;`fire` 为本次是否达到触发条件。
|
|
92
|
+
*/
|
|
93
|
+
export function trackLoop(state, turn, verdict, required = CONSECUTIVE_STEPS) {
|
|
94
|
+
const current = state ?? { consecutive: 0, lastTurn: Number.NaN };
|
|
95
|
+
const base = current.lastTurn === turn ? current.consecutive : 0;
|
|
96
|
+
if (verdict === 'insufficient')
|
|
97
|
+
return { state: { consecutive: base, lastTurn: turn }, fire: false };
|
|
98
|
+
const consecutive = verdict === 'loop' ? base + 1 : 0;
|
|
99
|
+
return { state: { consecutive, lastTurn: turn }, fire: consecutive >= required };
|
|
100
|
+
}
|
package/dist/throttle.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 提醒注入的节流:**同一 turn 至多一次**。
|
|
3
3
|
*
|
|
4
4
|
* 实测(2026-09-21,会话 `session-502b3e2b`,记录见设计方案 §12.4):漂移持续时**每个
|
|
5
5
|
* step 都会判定为漂移**,于是同一个 turn 内会连着注入多条几乎一样的提醒——观测到 3 连注,
|
|
@@ -8,13 +8,16 @@
|
|
|
8
8
|
* 所以收紧到「同一 turn 至多一次」:turn 是用户能感知的自然边界,重试留到下一轮,
|
|
9
9
|
* 而不是在一步之内反复催促。上限写成常数而不是配置项,是因为**没有数据支撑别的取值**;
|
|
10
10
|
* 等无对抗的自然漂移样本出来,再决定要不要放宽(设计方案 §八 第 9 条)。
|
|
11
|
+
*
|
|
12
|
+
* 退化提醒复用同一契约、独立一份状态:两类提醒的判据无关,共用状态会让先说的那类
|
|
13
|
+
* 把另一类挡在门外。
|
|
11
14
|
* @module @max-null/dsh-allostasis/throttle
|
|
12
15
|
*/
|
|
13
|
-
/**
|
|
14
|
-
export declare const
|
|
16
|
+
/** 同一 turn 内的提醒注入上限。 */
|
|
17
|
+
export declare const MAX_PER_TURN = 1;
|
|
15
18
|
/** 节流状态。不可变——每次放行都返回一份新状态。 */
|
|
16
|
-
export interface
|
|
17
|
-
/** 最近一次注入所针对的 turn
|
|
19
|
+
export interface PerTurnThrottleState {
|
|
20
|
+
/** 最近一次注入所针对的 turn(产出那段异常输出的 turn)。 */
|
|
18
21
|
lastTurn: number;
|
|
19
22
|
/** 该 turn 内已经注入了几次。 */
|
|
20
23
|
inTurn: number;
|
|
@@ -24,10 +27,10 @@ export interface AnchorThrottleState {
|
|
|
24
27
|
/**
|
|
25
28
|
* 判定本次是否放行,并推进状态。
|
|
26
29
|
*
|
|
27
|
-
* `sampledTurn`
|
|
30
|
+
* `sampledTurn` 是**产出那段被判定异常的输出的 turn**,不是当前 turn——提醒的措辞
|
|
28
31
|
* 指向「你上一步在想什么」,节流的计次也应当跟着它走。
|
|
29
32
|
* @param state - 上一次的状态;首次调用传 `undefined`。
|
|
30
|
-
* @param sampledTurn -
|
|
33
|
+
* @param sampledTurn - 产出该输出的 turn。
|
|
31
34
|
* @returns 放行时返回新状态;应当跳过时返回 `undefined`。
|
|
32
35
|
*/
|
|
33
|
-
export declare function
|
|
36
|
+
export declare function admitPerTurn(state: PerTurnThrottleState | undefined, sampledTurn: number): PerTurnThrottleState | undefined;
|
package/dist/throttle.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 提醒注入的节流:**同一 turn 至多一次**。
|
|
3
3
|
*
|
|
4
4
|
* 实测(2026-09-21,会话 `session-502b3e2b`,记录见设计方案 §12.4):漂移持续时**每个
|
|
5
5
|
* step 都会判定为漂移**,于是同一个 turn 内会连着注入多条几乎一样的提醒——观测到 3 连注,
|
|
@@ -8,23 +8,26 @@
|
|
|
8
8
|
* 所以收紧到「同一 turn 至多一次」:turn 是用户能感知的自然边界,重试留到下一轮,
|
|
9
9
|
* 而不是在一步之内反复催促。上限写成常数而不是配置项,是因为**没有数据支撑别的取值**;
|
|
10
10
|
* 等无对抗的自然漂移样本出来,再决定要不要放宽(设计方案 §八 第 9 条)。
|
|
11
|
+
*
|
|
12
|
+
* 退化提醒复用同一契约、独立一份状态:两类提醒的判据无关,共用状态会让先说的那类
|
|
13
|
+
* 把另一类挡在门外。
|
|
11
14
|
* @module @max-null/dsh-allostasis/throttle
|
|
12
15
|
*/
|
|
13
|
-
/**
|
|
14
|
-
export const
|
|
16
|
+
/** 同一 turn 内的提醒注入上限。 */
|
|
17
|
+
export const MAX_PER_TURN = 1;
|
|
15
18
|
/**
|
|
16
19
|
* 判定本次是否放行,并推进状态。
|
|
17
20
|
*
|
|
18
|
-
* `sampledTurn`
|
|
21
|
+
* `sampledTurn` 是**产出那段被判定异常的输出的 turn**,不是当前 turn——提醒的措辞
|
|
19
22
|
* 指向「你上一步在想什么」,节流的计次也应当跟着它走。
|
|
20
23
|
* @param state - 上一次的状态;首次调用传 `undefined`。
|
|
21
|
-
* @param sampledTurn -
|
|
24
|
+
* @param sampledTurn - 产出该输出的 turn。
|
|
22
25
|
* @returns 放行时返回新状态;应当跳过时返回 `undefined`。
|
|
23
26
|
*/
|
|
24
|
-
export function
|
|
27
|
+
export function admitPerTurn(state, sampledTurn) {
|
|
25
28
|
const current = state ?? { lastTurn: Number.NaN, inTurn: 0, count: 0 };
|
|
26
29
|
const inTurn = current.lastTurn === sampledTurn ? current.inTurn : 0;
|
|
27
|
-
if (inTurn >=
|
|
30
|
+
if (inTurn >= MAX_PER_TURN)
|
|
28
31
|
return undefined;
|
|
29
32
|
return { lastTurn: sampledTurn, inTurn: inTurn + 1, count: current.count + 1 };
|
|
30
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@max-null/dsh-allostasis",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "应变(allostasis):会话状态的自我调节。第一期:中文思考的漂移检测与近因锚定",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
"prepublishOnly": "npm run build"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"
|
|
51
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
55
|
-
"@deepseek-ai/dsh-agent": "
|
|
56
|
-
"@deepseek-ai/dsh-llm": "
|
|
57
|
-
"@deepseek-ai/dsh-session": "
|
|
55
|
+
"@deepseek-ai/dsh-agent": ">=0.1.7-rc.1 <0.3.0",
|
|
56
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0",
|
|
57
|
+
"@deepseek-ai/dsh-session": ">=0.1.7-rc.1 <0.3.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@types/node": "^26.2.0",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"typescript": "^5.5.0",
|
|
63
63
|
"vitest": "^2.1.0",
|
|
64
64
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
65
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-session": "^0.1.
|
|
65
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-session": "^0.1.7-rc.2"
|
|
68
68
|
}
|
|
69
69
|
}
|