@max-null/dsh-allostasis 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -9
- package/cordis.patch.yml +4 -0
- package/dist/client.js +238 -0
- package/dist/config.d.ts +68 -0
- package/dist/config.js +70 -0
- package/dist/drift.d.ts +7 -2
- package/dist/drift.js +8 -3
- package/dist/index.d.ts +41 -11
- package/dist/index.js +177 -35
- package/dist/messages.d.ts +62 -0
- package/dist/messages.js +89 -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/tail.d.ts +110 -0
- package/dist/tail.js +111 -0
- package/dist/throttle.d.ts +11 -8
- package/dist/throttle.js +10 -7
- package/docs/shots/silent-turn-1.png +0 -0
- package/package.json +26 -13
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
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**). When a turn ends with no visible output at all
|
|
11
|
+
(**silent turn**), it records the verdict in the session log and the browser half shows a
|
|
12
|
+
notice beneath that turn.
|
|
11
13
|
|
|
12
14
|
## 它做什么
|
|
13
15
|
|
|
@@ -16,10 +18,31 @@ message to bring it back.
|
|
|
16
18
|
| 能力 | 状态 | 触发条件 |
|
|
17
19
|
|---|---|---|
|
|
18
20
|
| **中文锚定** | **已实现** | 上一步思考的英文功能词密度越线 |
|
|
21
|
+
| **推理退化提醒** | **已实现** | 上一步思考的重复率越线,且连续 N 步成立 |
|
|
22
|
+
| **空回合提示** | **已实现** | 回合收尾时末条助手消息没有非空文本(只在 `completed` 回合上判) |
|
|
19
23
|
| 上下文占用感知 | 计划中 | 需先定「什么情况下才出现」(持续在场会退化成背景音) |
|
|
20
|
-
| 压缩预约落盘 | 计划中 |
|
|
24
|
+
| 压缩预约落盘 | 计划中 | 待通路验证:退化样本散布在整段退化区间,「只压一小段」能否打断循环尚无证据(二期方案 §八.1) |
|
|
21
25
|
|
|
22
|
-
|
|
26
|
+
前两类提醒以 `notice` 形式注入——在**轨迹页**是折叠态就显示一行摘要的注入行,不弹窗、不打断;
|
|
27
|
+
**对话页不显示**(2026-09-29 在 SSiD dev / DSH 0.2.0-rc.1 上实测,口径见二期方案 §九)。
|
|
28
|
+
|
|
29
|
+
**空回合提示是三者里唯一有界面的一个**,就挂在那一段空白下面(见「截图」段)。它不发消息、不改模型输入——
|
|
30
|
+
浏览器半边从 `turn/start` / `assistant/message` / `turn/end` 自己折叠出判定,投影成 Turn 尾部的一行提示。
|
|
31
|
+
|
|
32
|
+
**判定结果不写会话日志。** 内核的事件词汇表在构建期生成,下游插件的自定义类型不在其中,而 `Session.append()` 没有 `ignorable` 标记通道;无标记的自定义事件会让**整份**日志在下次加载时被拒读。判定输入本来就可重放,留痕由 `console.debug` 诊断行与界面上那行提示承担。理由与取证见 `docs/设计/2026-09-30-空回合检测与可见化.md` §十一。
|
|
33
|
+
|
|
34
|
+
**除空回合外全程静默,所以「确认它在工作」只能靠日志。** 前两类命中时只在轨迹页留一行;不命中时一个字都不说——「装了没有」「阈值生效没有」「这一步为什么没提醒」三个问题原本都无从回答,只能靠改配置去试。因此它在 `src/index.ts` 的 `apply` 里留两行痕:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
[dsh-allostasis] loaded · driftThreshold=0.15 repetitionThreshold=0.5 consecutiveSteps=2
|
|
38
|
+
[dsh-allostasis] turn 8 step 11 · drift=chinese funcDensity=0.6% chars=1764
|
|
39
|
+
· repetition=normal units=29 ratio=0%
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
第一行 `info`、每个进程一次,报的是**生效阈值**(`Config` 的解析结果,不是代码里的缺省常量)。第二行 `debug`、**每一步判定一行**:两类判据的三态结论加度量。默认静默,排查时打开即可——不必为了看一眼判定结果去动阈值。其中 `units` 是切分后的单元数,**低于 12 判 `insufficient`**(样本不足不下结论),此时 `ratio` 仍会给出,只是不参与判定。
|
|
43
|
+
|
|
44
|
+
判据来源、实测数据与完整设计见 `docs/设计/2026-09-20-应变-设计方案.md`、
|
|
45
|
+
`docs/设计/2026-09-28-应变二期-退化检测与自动干预.md`。
|
|
23
46
|
|
|
24
47
|
## 为什么需要它
|
|
25
48
|
|
|
@@ -29,6 +52,10 @@ message to bring it back.
|
|
|
29
52
|
|
|
30
53
|
## 判据
|
|
31
54
|
|
|
55
|
+
两类提醒各有一个纯函数判据,都在 `src/` 下,都附实测依据。
|
|
56
|
+
|
|
57
|
+
### 语言漂移
|
|
58
|
+
|
|
32
59
|
**英文功能词密度** =(`the` / `is` / `are` / `and` / `of` / `to` / `that` 这类功能词数)/ 英文词数,**且英文词数 ≥ 50 才判定**。
|
|
33
60
|
|
|
34
61
|
实测数据(一份 10 turn / 157 step 的真实会话):中文期中位 **0.012**、英文期中位 **0.273–0.389**,阈值 **0.15** 使两群完全分离。词数门槛不可省——中文期唯一的越线点只有 26 个英文词,小样本会让密度失真。
|
|
@@ -37,12 +64,25 @@ message to bring it back.
|
|
|
37
64
|
|
|
38
65
|
阈值由 `tools/analyze-thinking-lang.mjs` 在一份真实会话上标定,该工具随包发布。
|
|
39
66
|
|
|
67
|
+
### 推理退化
|
|
68
|
+
|
|
69
|
+
**重复率** = 单条推理按换行与中英句读切分后,**出现 ≥3 次的单元占全部单元的比例**;**单元数 ≥ 12 才判定**,**连续 N 步(默认 2)越线才触发**。
|
|
70
|
+
|
|
71
|
+
实测数据(一份 19 轮 / 416 条助手消息的真实会话):正常期 **0%–35%**、退化期 **48%–92%**,阈值 **0.5** 落在隔离带中段。连续步数不可省——正常期也有单次抖动(实测峰值 35%);代价是延迟,按同一份数据回放会晚约 3 步触发。
|
|
72
|
+
|
|
73
|
+
**退化与上下文占用脱钩**:同一会话里 26.1% 占用时重复率 84%、67.9% 时 83–92%,压缩到 26.1% 并不降低重复率。所以「调低阈值」治不了它;压缩能否治它取决于力度。完整数据见 `docs/排查/2026-09-28-推理退化与上下文占用脱钩.md`。
|
|
74
|
+
|
|
75
|
+
退化触发的留痕是 `console.debug` 诊断行(`repetition=` 那一段给出 `units` 与 `ratio`)加上轨迹页那行提醒。**判定结果不落会话日志**——理由与空回合那条同源,见前文。
|
|
76
|
+
|
|
40
77
|
## 截图
|
|
41
78
|
|
|
42
|
-
|
|
79
|
+
空回合提示:浏览器半边从会话事件流自行折叠出「这一轮没有产出内容」,对话页在该回合尾部显示一行说明。它贴着那一段空白,回看历史时也还在原处。
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
|
|
81
|
+
| 空回合提示 |
|
|
82
|
+
|---|
|
|
83
|
+
|  |
|
|
84
|
+
|
|
85
|
+
其余两个能力(中文锚定、推理退化提醒)是**提示注入类**:不新增按钮、面板或设置项,每个 step 前读取最近一条思考,判定越线时向请求末尾追加一条提醒,效果体现在模型行为上——那两项按《SSiD 开发手册》§9 走无 UI 插件豁免,只有空回合提示有界面元素。
|
|
46
86
|
|
|
47
87
|
## Compose
|
|
48
88
|
|
|
@@ -57,7 +97,27 @@ Installs as a bundle: `dsh plugin --profile <name> add @max-null/dsh-allostasis`
|
|
|
57
97
|
|
|
58
98
|
## Config
|
|
59
99
|
|
|
60
|
-
|
|
100
|
+
三个判定阈值加一个空回合档位可在 `config` 段覆盖;省略即用括号内的默认值。
|
|
101
|
+
|
|
102
|
+
| 字段 | 默认 | 含义 |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `driftThreshold` | `0.15` | 英文功能词密度达到此值即判为漂移 |
|
|
105
|
+
| `repetitionThreshold` | `0.5` | 推理重复率阈值,取值 0–1 |
|
|
106
|
+
| `consecutiveSteps` | `2` | 连续多少步越线才触发退化提醒 |
|
|
107
|
+
| `silentTurn` | `observe` | 空回合档位:`off` 宿主不判定也不干预、`observe` 判定并打一行诊断、`steer` 额外补一次生成 |
|
|
108
|
+
|
|
109
|
+
**档位管不到对话页那行提示**:提示由浏览器半边折叠会话事件流得出,与宿主判据同源但独立于档位——浏览器半边的 `apply` 拿不到插件配置(2026-10-01 实测:在 `cordis.patch.yml` 里配 `silentTurn: off`,宿主读到 `off`,浏览器半边收到空对象)。要完全静默请禁用插件。
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
- id: allostasis
|
|
113
|
+
name: '@max-null/dsh-allostasis'
|
|
114
|
+
config:
|
|
115
|
+
repetitionThreshold: 0.6
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
判据里的**小样本门槛**(`MIN_WORDS` / `MIN_UNITS` / `REPEAT_MIN_COUNT`)不是配置项——它们不是偏好,改了就是把密度与比例算飞。值域外的取值会让插件**报错中止**而不是静默回退:静默回退会让「我明明改了配置」与「插件按默认值跑」同时成立而无从察觉。
|
|
119
|
+
|
|
120
|
+
这几个阈值都是**起点值而非标定结果**——单会话样本给出的隔离带中段,标定留给数据积累(设计方案 §5.1 的三条路:显性配置项、数据积累、LLM 自调)。
|
|
61
121
|
|
|
62
122
|
## Develop
|
|
63
123
|
|
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/client.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@max-null/dsh-allostasis",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
//#region src/tail.ts
|
|
9
|
+
/**
|
|
10
|
+
* 统计一条助手消息的块构成。
|
|
11
|
+
*
|
|
12
|
+
* `text` 按去掉首尾空白后的长度计入:只含空白的文本块在界面上不产生任何可见内容,
|
|
13
|
+
* 把它算作「说了话」会让判据漏掉真实的空回合。
|
|
14
|
+
* @param turn - 该消息所属的 turn。
|
|
15
|
+
* @param step - 该消息所属的 step。
|
|
16
|
+
* @param content - 助手消息的内容块,按出现顺序。
|
|
17
|
+
* @returns 该消息的产出形态。
|
|
18
|
+
*/
|
|
19
|
+
function shapeOf(turn, step, content) {
|
|
20
|
+
let textChars = 0;
|
|
21
|
+
let reasoningChars = 0;
|
|
22
|
+
const blocks = {
|
|
23
|
+
reasoning: 0,
|
|
24
|
+
text: 0,
|
|
25
|
+
toolCalls: 0
|
|
26
|
+
};
|
|
27
|
+
for (const block of content) if (block.type === "reasoning") {
|
|
28
|
+
blocks.reasoning += 1;
|
|
29
|
+
reasoningChars += block.text?.length ?? 0;
|
|
30
|
+
} else if (block.type === "text") {
|
|
31
|
+
blocks.text += 1;
|
|
32
|
+
textChars += block.text?.trim().length ?? 0;
|
|
33
|
+
} else if (block.type === "tool-call") blocks.toolCalls += 1;
|
|
34
|
+
return {
|
|
35
|
+
turn,
|
|
36
|
+
step,
|
|
37
|
+
textChars,
|
|
38
|
+
reasoningChars,
|
|
39
|
+
blocks
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 对一次测量下判定。
|
|
44
|
+
*
|
|
45
|
+
* 只有「有助手消息且它没有非空文本」才算空回合。工具调用**不**改变判定:一个以
|
|
46
|
+
* `tool-call` 结尾的完成态回合同样是异常(工具执行完还会再走一步,除非回合已经结束)。
|
|
47
|
+
* @param shape - 末条助手消息的形态;没有助手消息时传 `undefined`。
|
|
48
|
+
* @returns 三态判定。
|
|
49
|
+
*/
|
|
50
|
+
function tailVerdict(shape) {
|
|
51
|
+
if (shape === void 0) return "unknown";
|
|
52
|
+
return shape.textChars > 0 ? "spoke" : "silent";
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/client/silent-turn.ts
|
|
56
|
+
/** Turn 数据键;同时用作 Definition 的 `kind`。 */
|
|
57
|
+
const SILENT_TURN_KEY = "allostasis-silent";
|
|
58
|
+
/**
|
|
59
|
+
* 对一个已收尾的 Turn 下判定。
|
|
60
|
+
* @param turn - 收尾的回合号。
|
|
61
|
+
* @param reason - `turn/end` 记录的收尾原因。
|
|
62
|
+
* @param tail - 本 Turn 末条助手消息的形态;没有助手消息时为 null。
|
|
63
|
+
* @returns 命中时渲染端要读到的事实,否则 null。
|
|
64
|
+
*/
|
|
65
|
+
function silentData(turn, reason, tail) {
|
|
66
|
+
if (reason.kind !== "completed" || tail === null) return null;
|
|
67
|
+
if (tailVerdict(tail) !== "silent") return null;
|
|
68
|
+
return {
|
|
69
|
+
turn,
|
|
70
|
+
textChars: tail.textChars,
|
|
71
|
+
reasoningChars: tail.reasoningChars
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 累积空回合判定。
|
|
76
|
+
*
|
|
77
|
+
* `match` 只读当前事件:`turn/start` 开一个 Turn,之后两条更新各自折进 State。历史窗口从
|
|
78
|
+
* 中间加载时 `turn/start` 可能不在窗口里,那种情况下本 Definition 不启动——没有开头就
|
|
79
|
+
* 无从知道这一轮是怎么开始的。
|
|
80
|
+
*/
|
|
81
|
+
const silentTurnDefinition = {
|
|
82
|
+
kind: SILENT_TURN_KEY,
|
|
83
|
+
match: (event) => {
|
|
84
|
+
if (event.type === "turn/start") return {
|
|
85
|
+
id: String(event.data.turn),
|
|
86
|
+
role: "start"
|
|
87
|
+
};
|
|
88
|
+
if (event.type === "assistant/message") return {
|
|
89
|
+
id: String(event.data.turn),
|
|
90
|
+
role: "update"
|
|
91
|
+
};
|
|
92
|
+
if (event.type === "turn/end") return {
|
|
93
|
+
id: String(event.data.turn),
|
|
94
|
+
role: "update"
|
|
95
|
+
};
|
|
96
|
+
return null;
|
|
97
|
+
},
|
|
98
|
+
start: (_context, match) => {
|
|
99
|
+
if (match.event.type !== "turn/start") throw new Error("allostasis-silent start requires turn/start");
|
|
100
|
+
return {
|
|
101
|
+
turn: match.event.data.turn,
|
|
102
|
+
tail: null,
|
|
103
|
+
found: null
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
update: (context, match) => {
|
|
107
|
+
const { event } = match;
|
|
108
|
+
if (event.type === "assistant/message") return {
|
|
109
|
+
...context.state,
|
|
110
|
+
turn: event.data.turn,
|
|
111
|
+
tail: shapeOf(event.data.turn, event.data.step, event.data.message.content)
|
|
112
|
+
};
|
|
113
|
+
if (event.type === "turn/end") return {
|
|
114
|
+
...context.state,
|
|
115
|
+
turn: event.data.turn,
|
|
116
|
+
found: silentData(event.data.turn, event.data.reason, context.state.tail)
|
|
117
|
+
};
|
|
118
|
+
return context.state;
|
|
119
|
+
},
|
|
120
|
+
buildLocationData: (context, scope, previous) => {
|
|
121
|
+
const { state } = context;
|
|
122
|
+
if (scope !== "turn" || state === void 0 || state.found === null) return null;
|
|
123
|
+
if (previous?.kind === "turn" && previous.turn === state.turn && previous.key === "allostasis-silent" && previous.value === state.found) return previous;
|
|
124
|
+
return {
|
|
125
|
+
kind: "turn",
|
|
126
|
+
turn: state.turn,
|
|
127
|
+
key: SILENT_TURN_KEY,
|
|
128
|
+
value: state.found
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
//#region src/client/SilentTurnNotice.tsx
|
|
134
|
+
/**
|
|
135
|
+
* 渲染本 Turn 的空回合提示;该 Turn 没有命中判定时什么都不渲染。
|
|
136
|
+
*
|
|
137
|
+
* 每个 list 条目都会对每一轮渲染一次,所以判定在这里收窄到自己的那两个键上。
|
|
138
|
+
* @param props - 回合尾部 owner 货币与本地化座位。
|
|
139
|
+
* @returns 提示条,或 `null`。
|
|
140
|
+
*/
|
|
141
|
+
function SilentTurnNotice({ turn, t }) {
|
|
142
|
+
const data = turn.data.get(SILENT_TURN_KEY);
|
|
143
|
+
if (data === void 0) return null;
|
|
144
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
145
|
+
style: wrap,
|
|
146
|
+
role: "status",
|
|
147
|
+
"data-allostasis-silent-turn": data.turn,
|
|
148
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
149
|
+
style: headline,
|
|
150
|
+
children: t("silent.headline")
|
|
151
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
152
|
+
style: hint,
|
|
153
|
+
children: data.reasoningChars > 0 ? t("silent.hintReasoned") : t("silent.hintEmpty")
|
|
154
|
+
})]
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const wrap = {
|
|
158
|
+
display: "flex",
|
|
159
|
+
flexDirection: "column",
|
|
160
|
+
gap: "2px",
|
|
161
|
+
margin: "6px 0 2px",
|
|
162
|
+
padding: "8px 12px",
|
|
163
|
+
borderLeft: "2px solid var(--dsw-alias-label-caption)",
|
|
164
|
+
borderRadius: "var(--dsw-radius-md)",
|
|
165
|
+
background: "var(--dsw-alias-bg-overlay)",
|
|
166
|
+
color: "var(--dsw-alias-label-secondary)",
|
|
167
|
+
fontFamily: "var(--dsw-font-family)",
|
|
168
|
+
fontSize: "12px",
|
|
169
|
+
lineHeight: "18px"
|
|
170
|
+
};
|
|
171
|
+
const headline = {
|
|
172
|
+
color: "var(--dsw-alias-label-primary)",
|
|
173
|
+
fontWeight: 600
|
|
174
|
+
};
|
|
175
|
+
const hint = { opacity: .85 };
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/client/locales.ts
|
|
178
|
+
/**
|
|
179
|
+
* 浏览器半边的文案字典。
|
|
180
|
+
*
|
|
181
|
+
* 产品可见字符串一律走这里,组件不内联文案(DSH 的 `verify-client-ui-i18n` 按来源
|
|
182
|
+
* 归属校验)。`LocaleNamespaceMap` 里登记的是 `keyof typeof en`,两套字典的键必须一致。
|
|
183
|
+
*
|
|
184
|
+
* 措辞对着**用户此刻的困惑**写:他看到的是空白,所以先说明发生了什么,再给一条出路。
|
|
185
|
+
* 不说「模型坏了」——那是归因,而这里只知道「这一轮没有产出」。
|
|
186
|
+
* @module @max-null/dsh-allostasis/client/locales
|
|
187
|
+
*/
|
|
188
|
+
/** 本插件在浏览器半边占用的 locale 命名空间。 */
|
|
189
|
+
const NS = "allostasis";
|
|
190
|
+
/** 中文文案。 */
|
|
191
|
+
const zh = {
|
|
192
|
+
/** 一行叙述:这条提醒说的是什么。 */
|
|
193
|
+
"silent.headline": "这一轮结束时模型没有产出内容",
|
|
194
|
+
/** 末步只生成了推理时的说明:给出可执行的下一步。 */
|
|
195
|
+
"silent.hintReasoned": "最后一步只生成了推理就停下了。可以再问一句让它接着说完,或直接看上面的操作记录。",
|
|
196
|
+
/** 末步什么块都没有时的说明。 */
|
|
197
|
+
"silent.hintEmpty": "最后一步没有产出任何内容。可以重问一次,或换一种说法。"
|
|
198
|
+
};
|
|
199
|
+
/** 英文文案;键与 {@link zh} 一一对应。 */
|
|
200
|
+
const en = {
|
|
201
|
+
"silent.headline": "This turn ended with no visible output",
|
|
202
|
+
"silent.hintReasoned": "The last step stopped after reasoning. Ask again to let it finish the thought, or read the operation log above.",
|
|
203
|
+
"silent.hintEmpty": "The last step produced nothing at all. Ask again, or rephrase the request."
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/client/index.tsx
|
|
207
|
+
/** 本插件在浏览器半边的包名,用作诊断行的前缀。 */
|
|
208
|
+
const PLUGIN_ID = "@max-null/dsh-allostasis";
|
|
209
|
+
/** 需要槽位注册表、文案服务与 Conversation 的事件注册表。 */
|
|
210
|
+
const inject = [
|
|
211
|
+
"slots",
|
|
212
|
+
"locale",
|
|
213
|
+
"uiConversation"
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* 登记文案与回合尾部座位。
|
|
217
|
+
* @param ctx - 浏览器半边上下文。
|
|
218
|
+
*/
|
|
219
|
+
function apply(ctx) {
|
|
220
|
+
console.info(`[${PLUGIN_ID}] client applied`);
|
|
221
|
+
ctx.effect(() => ctx.locale.register(NS, {
|
|
222
|
+
zh,
|
|
223
|
+
en
|
|
224
|
+
}), "dsh-allostasis: 文案字典");
|
|
225
|
+
ctx.effect(() => ctx.uiConversation.events.register(silentTurnDefinition), "dsh-allostasis: 空回合投影");
|
|
226
|
+
ctx.slots.inject("conversation.chat.turnTail", () => ctx.slots.register({
|
|
227
|
+
name: "conversation.chat.turnTail",
|
|
228
|
+
id: SILENT_TURN_KEY,
|
|
229
|
+
order: 10,
|
|
230
|
+
locale: NS
|
|
231
|
+
}, SilentTurnNotice));
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
exports.apply = apply;
|
|
235
|
+
exports.inject = inject;
|
|
236
|
+
return module.exports;
|
|
237
|
+
}
|
|
238
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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 { type SilentTurnMode } from './tail.ts';
|
|
19
|
+
/**
|
|
20
|
+
* 插件配置,与同名 schemastery schema 一起由 Loader 校验。
|
|
21
|
+
*
|
|
22
|
+
* 值域外的取值在 `resolveConfig` 里**报错中止**,不静默回退。
|
|
23
|
+
*/
|
|
24
|
+
export interface Config {
|
|
25
|
+
/** 英文功能词密度达到此值即判为漂移(默认 0.15)。实测中文期中位 0.012、英文期 0.273 以上。 */
|
|
26
|
+
driftThreshold?: number;
|
|
27
|
+
/** 重复率阈值,0–1(默认 0.5)。取 1 等于事实上关闭退化提醒。 */
|
|
28
|
+
repetitionThreshold?: number;
|
|
29
|
+
/** 连续多少步越线才触发退化提醒(默认 2)。取 1 会跟着正常期的单次抖动误报。 */
|
|
30
|
+
consecutiveSteps?: number;
|
|
31
|
+
/**
|
|
32
|
+
* 空回合兜底的档位(默认 `observe`)。
|
|
33
|
+
*
|
|
34
|
+
* `off` 宿主不判定也不干预;`observe` 判定并打一行诊断;`steer` 在判定之外追加一次
|
|
35
|
+
* 补生成请求。补生成受两条边界约束:同一回合至多一次(`throttle.ts`),取消的回合与
|
|
36
|
+
* 子代理会话不触发(见 `index.ts` 的 `installSilentTurn`)。
|
|
37
|
+
*
|
|
38
|
+
* **档位管不到对话页那行提示**:提示由浏览器半边折叠会话事件流得出,与宿主判据同源
|
|
39
|
+
* 但独立于档位——浏览器半边的 `apply` 拿不到插件配置(2026-10-01 实测)。要完全静默
|
|
40
|
+
* 就禁用插件。
|
|
41
|
+
*
|
|
42
|
+
* **默认取 `observe` 而不是 `steer`**:补生成能不能改变采样轨迹尚未验证
|
|
43
|
+
* (设计文档 §六.1),先让它只观测、由使用者显式打开。
|
|
44
|
+
*/
|
|
45
|
+
silentTurn?: SilentTurnMode;
|
|
46
|
+
}
|
|
47
|
+
/** {@link Config} 经校验后的形态:字段齐全,可直接参与判定。 */
|
|
48
|
+
export interface ResolvedConfig {
|
|
49
|
+
/** 见 {@link Config.driftThreshold}。 */
|
|
50
|
+
readonly driftThreshold: number;
|
|
51
|
+
/** 见 {@link Config.repetitionThreshold}。 */
|
|
52
|
+
readonly repetitionThreshold: number;
|
|
53
|
+
/** 见 {@link Config.consecutiveSteps}。 */
|
|
54
|
+
readonly consecutiveSteps: number;
|
|
55
|
+
/** 见 {@link Config.silentTurn}。 */
|
|
56
|
+
readonly silentTurn: SilentTurnMode;
|
|
57
|
+
}
|
|
58
|
+
/** Loader 用于校验 `cordis.patch.yml` 里 `config` 段的 schema。 */
|
|
59
|
+
export declare const Config: Schema<Config>;
|
|
60
|
+
/**
|
|
61
|
+
* 校验配置并补齐缺省值。
|
|
62
|
+
*
|
|
63
|
+
* 范围约束不交给 schema 的 `.min()/.max()`:那样报错只会说「number」,而说出该字段的
|
|
64
|
+
* 合理区间与收到的实际值,正是 fail-loud 的全部价值。
|
|
65
|
+
* @param config - Loader 传入的配置;未配置时传 `undefined` 或 `{}`。
|
|
66
|
+
* @returns 字段齐全且已校验的配置。
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveConfig(config?: Config): ResolvedConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
import { SILENT_TURN_MODE, SILENT_TURN_MODES } from "./tail.js";
|
|
21
|
+
/** Loader 用于校验 `cordis.patch.yml` 里 `config` 段的 schema。 */
|
|
22
|
+
export const Config = Schema.object({
|
|
23
|
+
driftThreshold: Schema.number(),
|
|
24
|
+
repetitionThreshold: Schema.number(),
|
|
25
|
+
consecutiveSteps: Schema.number(),
|
|
26
|
+
silentTurn: Schema.union(SILENT_TURN_MODES),
|
|
27
|
+
});
|
|
28
|
+
/**
|
|
29
|
+
* 校验一个 0–1 的阈值字段。
|
|
30
|
+
* @param field - 字段名,用于报错文案。
|
|
31
|
+
* @param value - 字段值。
|
|
32
|
+
* @returns 校验通过的原值。
|
|
33
|
+
*/
|
|
34
|
+
function ratio(field, value) {
|
|
35
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
36
|
+
throw new Error(`dsh-allostasis: \`${field}\` must be a number between 0 and 1, got ${value}`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 校验一个枚举字段。
|
|
42
|
+
* @param field - 字段名,用于报错文案。
|
|
43
|
+
* @param value - 字段值。
|
|
44
|
+
* @param allowed - 允许的取值。
|
|
45
|
+
* @returns 校验通过的原值。
|
|
46
|
+
*/
|
|
47
|
+
function oneOf(field, value, allowed) {
|
|
48
|
+
if (!allowed.includes(value)) {
|
|
49
|
+
throw new Error(`dsh-allostasis: \`${field}\` must be one of ${allowed.join(' | ')}, got ${String(value)}`);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* 校验配置并补齐缺省值。
|
|
55
|
+
*
|
|
56
|
+
* 范围约束不交给 schema 的 `.min()/.max()`:那样报错只会说「number」,而说出该字段的
|
|
57
|
+
* 合理区间与收到的实际值,正是 fail-loud 的全部价值。
|
|
58
|
+
* @param config - Loader 传入的配置;未配置时传 `undefined` 或 `{}`。
|
|
59
|
+
* @returns 字段齐全且已校验的配置。
|
|
60
|
+
*/
|
|
61
|
+
export function resolveConfig(config = {}) {
|
|
62
|
+
const driftThreshold = ratio('driftThreshold', config.driftThreshold ?? DRIFT_THRESHOLD);
|
|
63
|
+
const repetitionThreshold = ratio('repetitionThreshold', config.repetitionThreshold ?? REPETITION_THRESHOLD);
|
|
64
|
+
const consecutiveSteps = config.consecutiveSteps ?? CONSECUTIVE_STEPS;
|
|
65
|
+
if (!Number.isInteger(consecutiveSteps) || consecutiveSteps < 1) {
|
|
66
|
+
throw new Error(`dsh-allostasis: \`consecutiveSteps\` must be an integer >= 1, got ${consecutiveSteps}`);
|
|
67
|
+
}
|
|
68
|
+
const silentTurn = oneOf('silentTurn', config.silentTurn ?? SILENT_TURN_MODE, SILENT_TURN_MODES);
|
|
69
|
+
return { driftThreshold, repetitionThreshold, consecutiveSteps, silentTurn };
|
|
70
|
+
}
|
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/index.d.ts
CHANGED
|
@@ -1,33 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 应变(allostasis):会话状态的自我调节。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* 三个能力落在两个挂载点上:
|
|
5
|
+
*
|
|
6
|
+
* · **一期 · 中文锚定**(`agent/pre-step`):判定为语言漂移(英文功能词密度越线)就追加
|
|
7
|
+
* 一条中文锚定消息。
|
|
8
|
+
* · **二期 · 退化提醒**(`agent/pre-step`):判定为推理退化(重复率越线且连续若干步成立)
|
|
9
|
+
* 就追加减速提醒。
|
|
10
|
+
* · **空回合检测**(`agent/turn-stopping`):回合收尾时末条助手消息没有非空文本即判定成立,
|
|
11
|
+
* `steer` 档位下追加一次补生成。
|
|
12
|
+
*
|
|
13
|
+
* **判定结果不写会话日志**。会话日志的事件词汇表 `KNOWN_SESSION_EVENT_TYPES` 由内核在构建期
|
|
14
|
+
* 生成,下游插件的事件类型不在其中;`Session.append()` 的封装
|
|
15
|
+
* (`packages/core/session/src/index.ts:744-750`)没有 `ignorable` 通道,而
|
|
16
|
+
* `SessionEvent.ignorable` 的契约要求这类「丢失不影响重建」的记录必须自带该标记
|
|
17
|
+
* (`packages/core/session/src/types.ts:501-511`)。缺标记的自定义类型会让**整份**日志在下次
|
|
18
|
+
* 加载时被拒读(`packages/session/session-persistence/src/storage-contract.ts:75-80`)——
|
|
19
|
+
* 2026-10-01 实测:dev 与装版各有会话因此打不开。判定输入本来就可重放(推理原文在
|
|
20
|
+
* `reasoning-chunks.texts`、回合边界在 `turn/start` / `turn/end`),留痕因此改由本模块的
|
|
21
|
+
* `console.debug` 诊断行与浏览器半边的提示承担;浏览器半边自行从事件流折叠判定,
|
|
22
|
+
* 见 `client/silent-turn.ts`。
|
|
23
|
+
*
|
|
24
|
+
* 前两个共用 `agent/pre-step`,因为它们读同一份输入——最近一条思考。空回合读的是另一种
|
|
25
|
+
* 输入(这一轮最终产出了什么),而且**它之后没有下一步**,`pre-step` 不会被再次调用,
|
|
26
|
+
* 判定只能落在回合收尾。
|
|
27
|
+
*
|
|
28
|
+
* **平时不出现、异常时才出现**——按需出现是它作为信号的前提。但异常持续时也不每个 step
|
|
29
|
+
* 都提醒:同一 turn 至多一次,见 `throttle.ts`。两类提醒各有独立的一份节流状态:判据无关,
|
|
30
|
+
* 共用状态会让先说的那类把另一类挡在门外。
|
|
8
31
|
*
|
|
9
32
|
* 为什么不挂到 system prompt 前缀上:那是 `dsh-chinese-thinking` 的位置,它作为基线
|
|
10
33
|
* 永远在场。而基线在长会话里会失效(固定前缀离输出最远,语言模式受近因支配)。本插件
|
|
11
|
-
*
|
|
34
|
+
* 补的是「**你正在漂移 / 正在打转 / 这一轮什么也没说**」这类纠偏信号,因此必须落在近因
|
|
35
|
+
* 或收尾位置。
|
|
12
36
|
*
|
|
13
37
|
* 为什么用 `agent/pre-step` 而不是 `systemPrompt.context()`:前者直接给出 `turn` /
|
|
14
38
|
* `step`,且追加的是一条独立消息,落点比快照里的一个段更靠后。用法先例见官方
|
|
15
39
|
* `packages/context/time-context/src/index.ts:180-220`。
|
|
16
40
|
*
|
|
17
|
-
*
|
|
41
|
+
* 阈值与档位全部走 `config.ts` 的配置面。**压缩动作按设计方案 §九 暂缓**:数据指向退化
|
|
42
|
+
* 样本散布在整段退化区间,而 `compactRegion` 只压指定区间,能否打断循环尚未验证。
|
|
43
|
+
*
|
|
44
|
+
* 设计出处:`docs/设计/2026-09-20-应变-设计方案.md`、
|
|
45
|
+
* `docs/设计/2026-09-28-应变二期-退化检测与自动干预.md`、
|
|
46
|
+
* `docs/设计/2026-09-30-空回合检测与可见化.md`
|
|
18
47
|
* @module @max-null/dsh-allostasis
|
|
19
48
|
*/
|
|
20
49
|
import type { Context } from '@deepseek-ai/cordis';
|
|
21
50
|
import { anchorText } from './anchor.ts';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
51
|
+
import { Config } from './config.ts';
|
|
52
|
+
import { name } from './name.ts';
|
|
53
|
+
export { anchorText, Config, name };
|
|
54
|
+
/** 需要 `agents` 服务来接收 `agent/pre-step` 与 `agent/turn-stopping` 事件。 */
|
|
25
55
|
export declare const inject: string[];
|
|
26
|
-
export { anchorText };
|
|
27
56
|
/**
|
|
28
57
|
* 注册 pre-step 监听器;监听器随 `ctx` 生命周期销毁。
|
|
29
58
|
*
|
|
30
59
|
* 用 `{ prepend: true }` 以取得 `next()` 的决策后再追加,与官方 `time-context` 一致。
|
|
31
60
|
* @param ctx - 插件上下文。
|
|
61
|
+
* @param config - `cordis.patch.yml` 里的配置段;省略时全部取判据模块的缺省值。
|
|
32
62
|
*/
|
|
33
|
-
export declare function apply(ctx: Context): void;
|
|
63
|
+
export declare function apply(ctx: Context, config?: Config): void;
|