lume-dsh-plugin 0.6.2 → 0.7.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 +49 -5
- package/lib/core/ledger.js +211 -0
- package/lib/core/signals.js +64 -0
- package/lib/host/methods.js +64 -0
- package/lib/host/project.js +222 -0
- package/lib/host/reflection.js +27 -6
- package/lib/host/rpc.js +15 -0
- package/lib/host/session-runtime.js +10 -0
- package/lib/host/triggers.js +133 -0
- package/lib/index.js +358 -16
- package/package.json +2 -2
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 项目域:任务契约 / 改动台账 / 假设台账 / 项目知识。
|
|
3
|
+
*
|
|
4
|
+
* 四张表,两类归属:
|
|
5
|
+
* - **会话内**(键 = sessionId):contract、ledger、hypotheses —— 它们描述「这次任务」,
|
|
6
|
+
* 任务结束即无意义;
|
|
7
|
+
* - **跨会话**(键 = projectKey,由工作目录派生):facts —— 它们描述「这个仓库/这个
|
|
8
|
+
* 文档集合」,正是「越用越强」要积累的东西。放在会话键上会让换会话就失忆;
|
|
9
|
+
* 放在人设键上会让换人设就失忆(这是现有记忆的一个真实盲区)。
|
|
10
|
+
*
|
|
11
|
+
* 为什么项目知识不与人格记忆混用一张表:代码路径、构建命令、仓库约定属于**工作
|
|
12
|
+
* 事实**,不具备人格语义,也不该被人设卡的导出/分享带出去。
|
|
13
|
+
*
|
|
14
|
+
* 存储容错:与身份域同款——域不可用时整体降级为 null,功能缺失但不影响其余部分。
|
|
15
|
+
*/
|
|
16
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
17
|
+
import z from "@deepseek-ai/schemastery";
|
|
18
|
+
import { CHANGE_CAP, HYPOTHESIS_CAP, PROJECT_FACT_CAP, normalizeChange, normalizeProjectFact, trimChanges, trimFacts, } from "../core/ledger.js";
|
|
19
|
+
import { zodLike } from "./identity.js";
|
|
20
|
+
/** 存储里用 -1 表示「未估/未回填」:schemastery 的 number 不接受 null,避免为它引入联合类型。 */
|
|
21
|
+
const UNSET = -1;
|
|
22
|
+
export const LUME_PROJECT_SPEC = defineDomain({
|
|
23
|
+
name: "lume_project",
|
|
24
|
+
version: 1,
|
|
25
|
+
tables: {
|
|
26
|
+
/** 任务契约(键 = sessionId)。 */
|
|
27
|
+
contract: domainTable(zodLike(z.object({
|
|
28
|
+
goal: z.string(),
|
|
29
|
+
scope: z.array(z.string()),
|
|
30
|
+
expectCount: z.number(),
|
|
31
|
+
actualCount: z.number(),
|
|
32
|
+
criteria: z.array(z.string()),
|
|
33
|
+
nonGoals: z.array(z.string()),
|
|
34
|
+
open: z.array(z.string()),
|
|
35
|
+
at: z.number(),
|
|
36
|
+
turn: z.number(),
|
|
37
|
+
}))),
|
|
38
|
+
/** 改动台账(键 = sessionId)。 */
|
|
39
|
+
ledger: domainTable(zodLike(z.array(z.object({ target: z.string(), change: z.string(), why: z.string(), verify: z.string(), status: z.string(), at: z.number() })))),
|
|
40
|
+
/** 假设台账(键 = sessionId)。 */
|
|
41
|
+
hypotheses: domainTable(zodLike(z.array(z.object({ text: z.string(), evidence: z.string(), status: z.string(), at: z.number() })))),
|
|
42
|
+
/** 项目知识(键 = projectKey,跨会话共享)。 */
|
|
43
|
+
facts: domainTable(zodLike(z.array(z.object({ kind: z.string(), text: z.string(), at: z.number() })))),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
function toContract(stored) {
|
|
47
|
+
const raw = stored;
|
|
48
|
+
if (!raw || typeof raw.goal !== "string" || !raw.goal)
|
|
49
|
+
return null;
|
|
50
|
+
const list = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === "string") : []);
|
|
51
|
+
return {
|
|
52
|
+
goal: raw.goal,
|
|
53
|
+
scope: list(raw.scope),
|
|
54
|
+
expectCount: typeof raw.expectCount === "number" && raw.expectCount >= 0 ? raw.expectCount : null,
|
|
55
|
+
actualCount: typeof raw.actualCount === "number" && raw.actualCount >= 0 ? raw.actualCount : null,
|
|
56
|
+
criteria: list(raw.criteria),
|
|
57
|
+
nonGoals: list(raw.nonGoals),
|
|
58
|
+
open: list(raw.open),
|
|
59
|
+
at: typeof raw.at === "number" ? raw.at : 0,
|
|
60
|
+
turn: typeof raw.turn === "number" ? raw.turn : 0,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function fromContract(contract) {
|
|
64
|
+
return {
|
|
65
|
+
goal: contract.goal,
|
|
66
|
+
scope: contract.scope,
|
|
67
|
+
expectCount: contract.expectCount ?? UNSET,
|
|
68
|
+
actualCount: contract.actualCount ?? UNSET,
|
|
69
|
+
criteria: contract.criteria,
|
|
70
|
+
nonGoals: contract.nonGoals,
|
|
71
|
+
open: contract.open,
|
|
72
|
+
at: contract.at,
|
|
73
|
+
turn: contract.turn,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export class ProjectStore {
|
|
77
|
+
#contractTable;
|
|
78
|
+
#ledgerTable;
|
|
79
|
+
#hypothesisTable;
|
|
80
|
+
#factTable;
|
|
81
|
+
constructor(tables) {
|
|
82
|
+
this.#contractTable = tables.contract;
|
|
83
|
+
this.#ledgerTable = tables.ledger;
|
|
84
|
+
this.#hypothesisTable = tables.hypotheses;
|
|
85
|
+
this.#factTable = tables.facts;
|
|
86
|
+
}
|
|
87
|
+
// ── 契约 ──
|
|
88
|
+
getContract(sid) {
|
|
89
|
+
return toContract(this.#contractTable.get(sid));
|
|
90
|
+
}
|
|
91
|
+
async setContract(sid, contract) {
|
|
92
|
+
await this.#contractTable.put(sid, fromContract(contract));
|
|
93
|
+
}
|
|
94
|
+
/** 局部更新(回填数量、补判据、清待确认)。返回更新后的契约。 */
|
|
95
|
+
async patchContract(sid, patch) {
|
|
96
|
+
const current = this.getContract(sid);
|
|
97
|
+
if (!current)
|
|
98
|
+
return null;
|
|
99
|
+
const merged = {
|
|
100
|
+
...current,
|
|
101
|
+
...patch,
|
|
102
|
+
// 局部更新时 undefined 表示「不动这一项」;显式空数组视为清空。
|
|
103
|
+
scope: patch.scope ?? current.scope,
|
|
104
|
+
criteria: patch.criteria ?? current.criteria,
|
|
105
|
+
nonGoals: patch.nonGoals ?? current.nonGoals,
|
|
106
|
+
open: patch.open ?? current.open,
|
|
107
|
+
};
|
|
108
|
+
await this.setContract(sid, merged);
|
|
109
|
+
return merged;
|
|
110
|
+
}
|
|
111
|
+
// ── 改动台账 ──
|
|
112
|
+
getChanges(sid) {
|
|
113
|
+
const value = this.#ledgerTable.get(sid);
|
|
114
|
+
if (!Array.isArray(value))
|
|
115
|
+
return [];
|
|
116
|
+
return value
|
|
117
|
+
.map((entry) => {
|
|
118
|
+
const raw = entry;
|
|
119
|
+
const normalized = normalizeChange({ target: raw?.target, change: raw?.change, why: raw?.why, verify: raw?.verify, status: raw?.status }, typeof raw?.at === "number" ? raw.at : 0);
|
|
120
|
+
return normalized;
|
|
121
|
+
})
|
|
122
|
+
.filter((item) => item !== null);
|
|
123
|
+
}
|
|
124
|
+
/** 写入一条改动;同 target + 同 change 视为更新(状态推进),不重复建条目。 */
|
|
125
|
+
async upsertChange(sid, item) {
|
|
126
|
+
const items = this.getChanges(sid);
|
|
127
|
+
const index = items.findIndex((entry) => entry.target === item.target && entry.change === item.change);
|
|
128
|
+
if (index >= 0)
|
|
129
|
+
items[index] = { ...items[index], ...item };
|
|
130
|
+
else
|
|
131
|
+
items.push(item);
|
|
132
|
+
await this.#ledgerTable.put(sid, trimChanges(items, CHANGE_CAP));
|
|
133
|
+
}
|
|
134
|
+
/** 按 target 推进状态:模型常只报"改成 verified 了",不必重述 change 文本。 */
|
|
135
|
+
async setChangeStatus(sid, target, status) {
|
|
136
|
+
const items = this.getChanges(sid);
|
|
137
|
+
let hit = false;
|
|
138
|
+
for (const item of items) {
|
|
139
|
+
if (item.target !== target)
|
|
140
|
+
continue;
|
|
141
|
+
item.status = status;
|
|
142
|
+
hit = true;
|
|
143
|
+
}
|
|
144
|
+
if (hit)
|
|
145
|
+
await this.#ledgerTable.put(sid, trimChanges(items, CHANGE_CAP));
|
|
146
|
+
return hit;
|
|
147
|
+
}
|
|
148
|
+
// ── 假设台账 ──
|
|
149
|
+
getHypotheses(sid) {
|
|
150
|
+
const value = this.#hypothesisTable.get(sid);
|
|
151
|
+
if (!Array.isArray(value))
|
|
152
|
+
return [];
|
|
153
|
+
return value
|
|
154
|
+
.map((entry) => {
|
|
155
|
+
const raw = entry;
|
|
156
|
+
const text = typeof raw?.text === "string" ? raw.text : "";
|
|
157
|
+
if (!text)
|
|
158
|
+
return null;
|
|
159
|
+
const status = raw?.status;
|
|
160
|
+
return {
|
|
161
|
+
text,
|
|
162
|
+
evidence: typeof raw?.evidence === "string" ? raw.evidence : "",
|
|
163
|
+
status: (status === "testing" || status === "confirmed" || status === "excluded" ? status : "open"),
|
|
164
|
+
at: typeof raw?.at === "number" ? raw.at : 0,
|
|
165
|
+
};
|
|
166
|
+
})
|
|
167
|
+
.filter((item) => item !== null);
|
|
168
|
+
}
|
|
169
|
+
async upsertHypothesis(sid, item) {
|
|
170
|
+
const list = this.getHypotheses(sid);
|
|
171
|
+
const index = list.findIndex((entry) => entry.text === item.text);
|
|
172
|
+
if (index >= 0)
|
|
173
|
+
list[index] = { ...list[index], ...item };
|
|
174
|
+
else
|
|
175
|
+
list.push(item);
|
|
176
|
+
await this.#hypothesisTable.put(sid, list.slice(-HYPOTHESIS_CAP));
|
|
177
|
+
}
|
|
178
|
+
/** 一轮内至少更新过假设状态——触发器据此判断「有没有在维护假设」。 */
|
|
179
|
+
lastHypothesisAt(sid) {
|
|
180
|
+
return this.getHypotheses(sid).reduce((max, item) => Math.max(max, item.at), 0);
|
|
181
|
+
}
|
|
182
|
+
// ── 项目知识(跨会话)──
|
|
183
|
+
getFacts(projectKey) {
|
|
184
|
+
const value = this.#factTable.get(projectKey);
|
|
185
|
+
if (!Array.isArray(value))
|
|
186
|
+
return [];
|
|
187
|
+
return value
|
|
188
|
+
.map((entry) => {
|
|
189
|
+
const raw = entry;
|
|
190
|
+
return normalizeProjectFact({ kind: raw?.kind, text: raw?.text }, typeof raw?.at === "number" ? raw.at : 0);
|
|
191
|
+
})
|
|
192
|
+
.filter((item) => item !== null);
|
|
193
|
+
}
|
|
194
|
+
/** 追加项目事实;近似重复的忽略。返回是否写入。 */
|
|
195
|
+
async addFact(projectKey, fact, isDuplicate) {
|
|
196
|
+
const facts = this.getFacts(projectKey);
|
|
197
|
+
if (isDuplicate(fact.text, facts))
|
|
198
|
+
return false;
|
|
199
|
+
facts.push(fact);
|
|
200
|
+
await this.#factTable.put(projectKey, trimFacts(facts, PROJECT_FACT_CAP));
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
async deleteFact(projectKey, index) {
|
|
204
|
+
const facts = this.getFacts(projectKey);
|
|
205
|
+
if (index < 0 || index >= facts.length)
|
|
206
|
+
return false;
|
|
207
|
+
facts.splice(index, 1);
|
|
208
|
+
await this.#factTable.put(projectKey, facts);
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
async clearFacts(projectKey) {
|
|
212
|
+
await this.#factTable.delete(projectKey);
|
|
213
|
+
}
|
|
214
|
+
/** 会话结束清理:任务态数据不跨会话保留(项目知识是另一张表,不受影响)。 */
|
|
215
|
+
async clearSession(sid) {
|
|
216
|
+
await Promise.all([this.#contractTable.delete(sid), this.#ledgerTable.delete(sid), this.#hypothesisTable.delete(sid)]);
|
|
217
|
+
}
|
|
218
|
+
/** 诊断用:当前项目键下的事实条数。 */
|
|
219
|
+
factCount(projectKey) {
|
|
220
|
+
return this.getFacts(projectKey).length;
|
|
221
|
+
}
|
|
222
|
+
}
|
package/lib/host/reflection.js
CHANGED
|
@@ -14,6 +14,8 @@ export const LUME_REFLECTION_SPEC = defineDomain({
|
|
|
14
14
|
version: 1,
|
|
15
15
|
tables: {
|
|
16
16
|
logs: domainTable(zodLike(z.union([
|
|
17
|
+
z.object({ at: z.number(), context: z.number(), planning: z.number(), verification: z.number(), review: z.number(), diagnosis: z.number(), note: z.string() }),
|
|
18
|
+
// v0.7.0 之前的四维日志
|
|
17
19
|
z.object({ at: z.number(), context: z.number(), planning: z.number(), verification: z.number(), review: z.number(), note: z.string() }),
|
|
18
20
|
// v0.4.0 之前的存量日志;仅用于打开域并在启动时迁移。
|
|
19
21
|
z.object({ at: z.number(), p0: z.number(), p1: z.number(), p2: z.number(), p3: z.number(), note: z.string() }),
|
|
@@ -46,6 +48,8 @@ export class ReflectionStore {
|
|
|
46
48
|
planning: raw.p1,
|
|
47
49
|
verification: raw.p2,
|
|
48
50
|
review: raw.p3,
|
|
51
|
+
// 旧日志没有第五维:记 -1,统计时跳过(不当作 0 分)。
|
|
52
|
+
diagnosis: -1,
|
|
49
53
|
note: typeof raw.note === "string" ? raw.note : "",
|
|
50
54
|
});
|
|
51
55
|
migrated++;
|
|
@@ -68,17 +72,30 @@ export class ReflectionStore {
|
|
|
68
72
|
const entries = [...this.#table.keys()].map((key) => this.#table.get(key)).filter((e) => typeof e?.context === "number").sort((a, b) => b.at - a.at).slice(0, 5);
|
|
69
73
|
if (entries.length < 3)
|
|
70
74
|
return null;
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const dims = ["context", "planning", "verification", "review", "diagnosis"];
|
|
76
|
+
// 逐维在「评过这一维」的条目上取平均:老日志没有 diagnosis(-1),不参与该维统计,
|
|
77
|
+
// 否则会被当成 0 分,把一个从未评过的维度误报成最弱项。
|
|
78
|
+
const avg = (key) => {
|
|
79
|
+
const scored = entries.filter((e) => typeof e[key] === "number" && e[key] >= 0);
|
|
80
|
+
if (scored.length === 0)
|
|
81
|
+
return null;
|
|
82
|
+
return scored.reduce((n, e) => n + (e[key] ?? 0), 0) / scored.length;
|
|
83
|
+
};
|
|
84
|
+
const ranked = dims
|
|
85
|
+
.map((key) => ({ key, value: avg(key) }))
|
|
86
|
+
.filter((item) => item.value !== null)
|
|
87
|
+
.sort((a, b) => a.value - b.value);
|
|
88
|
+
const weakest = ranked[0];
|
|
89
|
+
if (!weakest || weakest.value > 1.15)
|
|
74
90
|
return null;
|
|
75
91
|
const text = {
|
|
76
92
|
context: "请先确认目标、约束和当前状态,避免遗漏已知信息。",
|
|
77
93
|
planning: "请按任务复杂度先做必要调研和计划,不要过早执行。",
|
|
78
94
|
verification: "本轮修改或执行后请立即做最小验证,不要只看命令是否结束。",
|
|
79
95
|
review: "完成前请对照需求、边界条件和数据保留做一次结果复核。",
|
|
96
|
+
diagnosis: "排查时先立假设再动手:写下每条假设的证据与状态,把已排除的标出来,不要重复验证同一个假设。",
|
|
80
97
|
};
|
|
81
|
-
return text[weakest];
|
|
98
|
+
return text[weakest.key];
|
|
82
99
|
}
|
|
83
100
|
}
|
|
84
101
|
export const REFLECTION_SYSTEM = [
|
|
@@ -89,8 +106,9 @@ export const REFLECTION_SYSTEM = [
|
|
|
89
106
|
"计划与门控:是否拆解任务、先调研再执行,并按风险自适应投入",
|
|
90
107
|
"验证与失败处理:是否在变更后验证,失败时归因并更换方案;引用日志/历史/旧报错作为证据时是否核对时间戳与因果归属,有没有把历史错误当成本次问题的原因",
|
|
91
108
|
"结果复核:是否对照完成标准、边界条件、兼容性和数据保留进行复核",
|
|
109
|
+
"诊断深度与假设管理:是否在排查时建立并维护假设(证据、状态、已排除项),是否避免重复验证同一个假设、避免撒网式浏览代替链路定位",
|
|
92
110
|
"",
|
|
93
|
-
|
|
111
|
+
'只输出一个 JSON 对象,形如 {"context":2,"planning":2,"verification":1,"review":0,"diagnosis":1,"note":"..."},note 一句话中文,不要输出其他内容。',
|
|
94
112
|
].join("\n");
|
|
95
113
|
export function buildReflectionPrompt(turns) {
|
|
96
114
|
return {
|
|
@@ -137,10 +155,13 @@ export function parseReflectionScore(output) {
|
|
|
137
155
|
const planning = clampScore(r.planning ?? r.p1);
|
|
138
156
|
const verification = clampScore(r.verification ?? r.p2);
|
|
139
157
|
const review = clampScore(r.review ?? r.p3);
|
|
158
|
+
// 第五维「诊断深度与假设管理」:模型没给这一维时记 -1(统计时跳过该维,不当作 0 分)。
|
|
159
|
+
const rawDiagnosis = clampScore(r.diagnosis ?? r.p4);
|
|
160
|
+
const diagnosis = Number.isNaN(rawDiagnosis) ? -1 : rawDiagnosis;
|
|
140
161
|
const note = typeof r.note === "string" ? r.note.trim().slice(0, 200) : "";
|
|
141
162
|
if (Number.isNaN(context) || Number.isNaN(planning) || Number.isNaN(verification) || Number.isNaN(review))
|
|
142
163
|
return null;
|
|
143
|
-
return { at: Date.now(), context, planning, verification, review, note };
|
|
164
|
+
return { at: Date.now(), context, planning, verification, review, diagnosis, note };
|
|
144
165
|
}
|
|
145
166
|
function clampScore(value) {
|
|
146
167
|
const n = Number(value);
|
package/lib/host/rpc.js
CHANGED
|
@@ -20,6 +20,21 @@ export function createLumeRpcHandler(deps) {
|
|
|
20
20
|
case "list": {
|
|
21
21
|
return { ok: true, value: registry.list() };
|
|
22
22
|
}
|
|
23
|
+
case "getProjectState": {
|
|
24
|
+
// 任务载具 + 项目知识的只读视图(诊断/排查用;未接线时返回 null)。
|
|
25
|
+
const sessionId = requireString(payload, "sessionId");
|
|
26
|
+
if (!sessionId)
|
|
27
|
+
return { ok: false, error: { code: "bad-request", message: "sessionId is required" } };
|
|
28
|
+
return { ok: true, value: deps.getProjectState?.(sessionId) ?? null };
|
|
29
|
+
}
|
|
30
|
+
case "clearProjectFacts": {
|
|
31
|
+
// 清空当前项目的知识积累(不可逆)。项目知识是工作事实,用户有权一键忘掉。
|
|
32
|
+
const sessionId = requireString(payload, "sessionId");
|
|
33
|
+
if (!sessionId)
|
|
34
|
+
return { ok: false, error: { code: "bad-request", message: "sessionId is required" } };
|
|
35
|
+
const cleared = (await deps.clearProjectFacts?.(sessionId)) ?? false;
|
|
36
|
+
return { ok: true, value: { cleared } };
|
|
37
|
+
}
|
|
23
38
|
case "select": {
|
|
24
39
|
const sessionId = requireString(payload, "sessionId");
|
|
25
40
|
const personaName = requireString(payload, "personaName");
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { newTriggerCounters } from "./triggers.js";
|
|
1
2
|
/** 运行时状态上限:与 PersonaStore 的 maxSessions 对齐,超限淘汰最旧。 */
|
|
2
3
|
const MAX_RUNTIME_SESSIONS = 200;
|
|
3
4
|
function defaultRuntime() {
|
|
@@ -22,6 +23,15 @@ function defaultRuntime() {
|
|
|
22
23
|
interactionMode: "question",
|
|
23
24
|
intent: null,
|
|
24
25
|
stableDigest: null,
|
|
26
|
+
projectKey: null,
|
|
27
|
+
toolKind: "other",
|
|
28
|
+
triggerCounters: newTriggerCounters(),
|
|
29
|
+
triggerNudge: null,
|
|
30
|
+
triggerFiredAt: {},
|
|
31
|
+
turnNudge: null,
|
|
32
|
+
lastDriftTurn: null,
|
|
33
|
+
knowledgePrompted: false,
|
|
34
|
+
hypothesesTouched: false,
|
|
25
35
|
alignmentCorrection: null,
|
|
26
36
|
recentUserQueries: [],
|
|
27
37
|
postTurnReview: null,
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 行为触发器:把「元决策」从用户手里接过来。
|
|
3
|
+
*
|
|
4
|
+
* 实测的症状不是模型不知道规矩,而是**在压力下不执行**:连续 34 次广度探查不收敛、
|
|
5
|
+
* 连续 18 次改动不验证、在同一个不可用的构建环境上撞 17 次、交付前不自审——每一次
|
|
6
|
+
* 都是用户亲自下令才纠正。协议文本管不了这种,因为文本是静态的,而症状是**轨迹**的。
|
|
7
|
+
*
|
|
8
|
+
* 所以这里只在**行为模式成立**时注入一句针对性的提醒,并且:
|
|
9
|
+
* - 每类触发器每轮最多一次、且有轮级冷却(提示一多就变噪音,模型会学会忽略);
|
|
10
|
+
* - 文本带具体数字("已连续 14 次只读探查"),让提醒可被核对而不是空洞训话;
|
|
11
|
+
* - 纯函数:计数器由调用方持有(SessionRuntime),判定与措辞在这里可单测。
|
|
12
|
+
*/
|
|
13
|
+
import { deadPathKind } from "../core/signals.js";
|
|
14
|
+
export function newTriggerCounters() {
|
|
15
|
+
return { inspectStreak: 0, mutateStreak: 0, verifyFailStreak: 0, verifyEnvHits: 0, mutations: 0, steps: 0 };
|
|
16
|
+
}
|
|
17
|
+
/** 计数器推进:语义是「行为模式」,因此只在类别切换或验证成败时重置。 */
|
|
18
|
+
export function applyToolSignal(counters, kind, signals) {
|
|
19
|
+
counters.steps++;
|
|
20
|
+
if (kind === "inspect") {
|
|
21
|
+
counters.inspectStreak++;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (kind === "mutate") {
|
|
25
|
+
counters.mutateStreak++;
|
|
26
|
+
counters.mutations++;
|
|
27
|
+
counters.inspectStreak = 0;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (kind === "verify") {
|
|
31
|
+
counters.inspectStreak = 0;
|
|
32
|
+
counters.mutateStreak = 0;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (kind === "plan") {
|
|
36
|
+
// 写载具本身就是「收敛」动作:两个连击都清零。
|
|
37
|
+
counters.inspectStreak = 0;
|
|
38
|
+
counters.mutateStreak = 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 验证结果的成败记账(在 tool/result 阶段调用,与 applyToolSignal 配对)。
|
|
43
|
+
* 失败连击只被**成功的验证**清零——普通探查不该让「死路」计数归零,
|
|
44
|
+
* 否则在死路上反复穿插读文件就能把提醒刷掉。
|
|
45
|
+
*/
|
|
46
|
+
export function applyVerifyOutcome(counters, kind, signals) {
|
|
47
|
+
if (kind !== "verify" || signals.unknown)
|
|
48
|
+
return;
|
|
49
|
+
if (signals.failure) {
|
|
50
|
+
counters.verifyFailStreak++;
|
|
51
|
+
if (signals.env)
|
|
52
|
+
counters.verifyEnvHits++;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
counters.verifyFailStreak = 0;
|
|
56
|
+
counters.verifyEnvHits = 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export const DEFAULT_TRIGGER_THRESHOLDS = {
|
|
60
|
+
inspectStreak: 12,
|
|
61
|
+
changeStreak: 6,
|
|
62
|
+
deadPathFails: 3,
|
|
63
|
+
knowledgeSteps: 20,
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* 工具事件触发的判定。一次只返回**一个**(按紧急度排序):同时堆三条提醒会互相稀释。
|
|
67
|
+
* 调用方负责「每类每轮最多一次」的冷却。
|
|
68
|
+
*/
|
|
69
|
+
export function evaluateToolTrigger(counters, ctx, thresholds = DEFAULT_TRIGGER_THRESHOLDS) {
|
|
70
|
+
const dead = deadPathKind(counters.verifyEnvHits, counters.verifyFailStreak);
|
|
71
|
+
if (dead !== null && counters.verifyFailStreak >= thresholds.deadPathFails) {
|
|
72
|
+
const n = counters.verifyFailStreak;
|
|
73
|
+
const m = counters.verifyEnvHits;
|
|
74
|
+
if (dead === "env") {
|
|
75
|
+
return {
|
|
76
|
+
id: "dead-path",
|
|
77
|
+
text: `〔验证降级〕同一环境已连续 ${n} 次验证失败,其中 ${m} 次是环境或依赖不可用(不是代码问题)。停止重复同一条命令,换降级阶梯:① 能用的编译器/测试 → ② 语法检查(parser / typecheck)→ ③ 静态交叉引用(谁调用它、它调用谁、配置与 SQL 绑定)→ ④ 手工走读并列出风险点。交付时明确写「本环境无法完成构建验证」。`,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
id: "dead-path",
|
|
82
|
+
text: `〔死路提醒〕同一验证已连续失败 ${n} 次。先归因(输入 / 逻辑 / 接口 / 环境 / 权限),把结论写进假设台账:证实的标 confirmed、排除的标 excluded(用 lume_hypothesis)——已排除的假设不要再试。换一个方案再动手。`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (counters.mutateStreak >= thresholds.changeStreak) {
|
|
86
|
+
return {
|
|
87
|
+
id: "verify-as-you-go",
|
|
88
|
+
text: `〔增量验证〕已连续 ${counters.mutateStreak} 次改动,中间没有任何验证动作。改一处验一处:现在先跑一次最小验证(编译 / 语法检查 / 回读改动区域),确认前一批改动真的生效,再继续下一批。一大批改完再验,失败时无法定位是哪一处的问题。`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (!ctx.hasContract && ctx.isTask && counters.mutations > 0) {
|
|
92
|
+
return {
|
|
93
|
+
id: "contract-missing",
|
|
94
|
+
text: "〔载具缺失〕你已经动手改动,但还没写下任务契约。花一次调用写清:目标(可观察的结果)、范围(精确到路径/模块/章节)、预计数量、完成判据(可执行)、非目标(明确不动什么)、待确认(≤2 个)。之后每步以契约为准,交付时按它逐项对账——用 lume_contract。",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (ctx.isTask && counters.inspectStreak >= thresholds.inspectStreak) {
|
|
98
|
+
return {
|
|
99
|
+
id: "converge",
|
|
100
|
+
text: `〔收敛提醒〕已连续 ${counters.inspectStreak} 次只读探查,还没有产出契约或改动台账。停止撒网式通读,先把链路复述出来——入口 → 数据流 → 影响面(谁调用、被谁调用、配置与 SQL 绑定)——写成改动台账(lume_change)并回填实际数量,然后带着这份清单回去读缺口。`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (counters.verifyFailStreak > 0 && ctx.diagnosing && ctx.hasHypotheses && !ctx.hypothesesTouched) {
|
|
104
|
+
return {
|
|
105
|
+
id: "hypothesis-stale",
|
|
106
|
+
text: "〔假设台账〕本轮出现了验证失败,但假设状态没有更新。把这次失败归因写进 lume_hypothesis(证实 / 排除 / 新假设),并标出下一步要验的是哪一条——否则同一个假设会被反复试。",
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
/** 轮边界触发的判定:契约对账(防判据漂移)与项目知识采集。 */
|
|
112
|
+
export function evaluateTurnTrigger(ctx, thresholds = DEFAULT_TRIGGER_THRESHOLDS) {
|
|
113
|
+
if (ctx.hasContract) {
|
|
114
|
+
const afterCompaction = ctx.compactionTurn !== null && ctx.turnIndex - ctx.compactionTurn <= 1;
|
|
115
|
+
const periodic = ctx.turnIndex >= 3 && (ctx.lastDriftTurn === null || ctx.turnIndex - ctx.lastDriftTurn >= 3);
|
|
116
|
+
if (afterCompaction || periodic)
|
|
117
|
+
return { id: "criteria-drift", text: "" }; // 文本由调用方按契约渲染
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (!ctx.knowledgePrompted && ctx.counters.steps >= thresholds.knowledgeSteps) {
|
|
121
|
+
return {
|
|
122
|
+
id: "knowledge-capture",
|
|
123
|
+
text: `〔项目知识〕本会话已执行 ${ctx.counters.steps} 步工具。若这轮确认了**稳定的**项目事实——构建/测试命令、模块数据流、仓库约定、或一条死路——用 lume_project_note 记下来(按当前工作目录跨会话累积,下次直接可用)。没有就忽略这句。`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/** 冷却判定:同一触发器在 N 轮内不再重复(提示变噪音就失效)。 */
|
|
129
|
+
export function cooldownOk(lastFiredTurn, turnIndex, cooldownTurns = 2) {
|
|
130
|
+
if (lastFiredTurn === null || lastFiredTurn === undefined)
|
|
131
|
+
return true;
|
|
132
|
+
return turnIndex - lastFiredTurn >= cooldownTurns;
|
|
133
|
+
}
|