lume-dsh-plugin 0.7.4 → 0.8.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/CHANGELOG.md +460 -0
- package/README.md +245 -275
- package/lib/client.js +242 -181
- package/lib/core/card.js +11 -9
- package/lib/core/citations.js +235 -0
- package/lib/core/coverage.js +149 -0
- package/lib/core/dialogue-mining.js +49 -11
- package/lib/core/knowledge.js +165 -0
- package/lib/core/leak-detector.js +1 -3
- package/lib/core/ledger.js +118 -14
- package/lib/core/manifest.js +3 -1
- package/lib/core/memory-id.js +105 -0
- package/lib/core/metrics.js +270 -0
- package/lib/core/persona-limits.js +25 -0
- package/lib/core/scope.js +124 -0
- package/lib/core/signals.js +285 -5
- package/lib/core/task-memory.js +143 -0
- package/lib/core/text.js +45 -3
- package/lib/host/backfill.js +218 -0
- package/lib/host/bootstrap.js +130 -0
- package/lib/host/boundary.js +1 -3
- package/lib/host/clauses.js +180 -0
- package/lib/host/config.js +7 -0
- package/lib/host/diag.js +44 -7
- package/lib/host/distill-prompt.js +365 -0
- package/lib/host/distill.js +22 -348
- package/lib/host/extraction.js +11 -3
- package/lib/host/host-context.js +1 -0
- package/lib/host/host-events.js +100 -0
- package/lib/host/identity.js +12 -29
- package/lib/host/inbound.js +133 -0
- package/lib/host/injection.js +2 -6
- package/lib/host/llm-aux.js +130 -0
- package/lib/host/llm-route.js +3 -0
- package/lib/host/methods.js +180 -10
- package/lib/host/metrics-log.js +169 -0
- package/lib/host/notices.js +62 -0
- package/lib/host/project-access.js +210 -0
- package/lib/host/project.js +153 -2
- package/lib/host/prompt-blocks.js +113 -0
- package/lib/host/protocol.js +142 -18
- package/lib/host/reflection.js +28 -5
- package/lib/host/registry.js +0 -4
- package/lib/host/requirements-scan.js +108 -0
- package/lib/host/rpc-bridge.js +23 -4
- package/lib/host/rpc.js +1 -1
- package/lib/host/sections.js +48 -0
- package/lib/host/session-deps.js +27 -0
- package/lib/host/session-events.js +371 -0
- package/lib/host/session-runtime.js +17 -5
- package/lib/host/thinking.js +10 -1
- package/lib/host/tools.js +367 -0
- package/lib/host/triggers.js +25 -1
- package/lib/host/turn-boundary.js +116 -0
- package/lib/host/wiring.js +280 -0
- package/lib/host/workspace-map.js +81 -0
- package/lib/index.js +326 -877
- package/package.json +11 -5
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { pathKey } from "../core/citations.js";
|
|
3
|
+
export function registerLumeTools(deps) {
|
|
4
|
+
// ── 模型可调用工具(主写入通道)──
|
|
5
|
+
// 工具 output schema 的 const 语义要求成功值恒为 { ok: true };失败一律抛错交由框架呈现。
|
|
6
|
+
// as const 让 defineTool 从字面量推断 O,三个工具共用同一份成功形状。
|
|
7
|
+
const OK_OUTPUT_SCHEMA = {
|
|
8
|
+
type: "object",
|
|
9
|
+
additionalProperties: false,
|
|
10
|
+
properties: { ok: { type: "boolean", const: true, required: true } },
|
|
11
|
+
};
|
|
12
|
+
// 度量工具要把摘要回给模型(唯一一个「读」工具),所以 schema 里带一段文本。
|
|
13
|
+
const METRICS_OUTPUT_SCHEMA = {
|
|
14
|
+
type: "object",
|
|
15
|
+
additionalProperties: false,
|
|
16
|
+
properties: {
|
|
17
|
+
ok: { type: "boolean", const: true, required: true },
|
|
18
|
+
text: { type: "string", required: true, description: "人读度量摘要" },
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
function dutyPersona(exec) {
|
|
22
|
+
const sid = exec?.agent?.session?.id;
|
|
23
|
+
const st = sid !== undefined ? deps.runtime.get(String(sid)) : undefined;
|
|
24
|
+
return st?.lastInjected ?? deps.defaultName;
|
|
25
|
+
}
|
|
26
|
+
deps.ctx.effect(() => {
|
|
27
|
+
deps.ctx.tools.register(defineTool({
|
|
28
|
+
name: "lume_remember",
|
|
29
|
+
description: "记住关于用户或你们关系的持久事实(偏好、习惯、背景、称呼)。仅当信息明确值得长期记住时调用;每次一条,40 字以内。不要记录工作内容、代码或项目机密。",
|
|
30
|
+
parameters: {
|
|
31
|
+
text: { type: "string", required: true, description: "要长期记住的事实,第三人称陈述句,≤40 字" },
|
|
32
|
+
},
|
|
33
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
|
|
34
|
+
execute: async (args, exec) => {
|
|
35
|
+
if (!deps.identity)
|
|
36
|
+
throw new Error("lume deps.identity store is unavailable");
|
|
37
|
+
const personaName = dutyPersona(exec);
|
|
38
|
+
if (!personaName)
|
|
39
|
+
throw new Error("lume_remember requires an active persona (当前没有当值人设)");
|
|
40
|
+
await deps.identity.addMemory(personaName, String(args.text), deps.isDuplicateFact);
|
|
41
|
+
return { ok: true };
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
deps.ctx.tools.register(defineTool({
|
|
45
|
+
name: "lume_update_style",
|
|
46
|
+
description: "把用户对你说话方式的新要求固化为长期风格约定(如「少用 emoji」「自称改成XX」)。仅当用户明确提出风格/语气要求时调用,每条一句话。",
|
|
47
|
+
parameters: {
|
|
48
|
+
rule: { type: "string", required: true, description: "风格约定,一句话祈使句" },
|
|
49
|
+
},
|
|
50
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
|
|
51
|
+
execute: async (args, exec) => {
|
|
52
|
+
if (!deps.identity)
|
|
53
|
+
throw new Error("lume deps.identity store is unavailable");
|
|
54
|
+
const personaName = dutyPersona(exec);
|
|
55
|
+
if (!personaName)
|
|
56
|
+
throw new Error("lume_update_style requires an active persona (当前没有当值人设)");
|
|
57
|
+
await deps.identity.addStyleRule(personaName, String(args.rule), (a, b) => deps.jaccard(a, b) >= 0.6);
|
|
58
|
+
return { ok: true };
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
deps.ctx.tools.register(defineTool({
|
|
62
|
+
name: "lume_create_persona",
|
|
63
|
+
description: "创建一个全新的自定义人设。仅当用户明确想新建人设时使用:先在对话中访谈收集(人设的名字、性格、说话方式、对用户的称呼),收集完整后再调用本工具保存,并告知用户保存成功。",
|
|
64
|
+
parameters: {
|
|
65
|
+
name: { type: "string", required: true, description: "人设英文键名,小写字母开头,≤32 字符(如 tsundere)" },
|
|
66
|
+
displayName: { type: "string", required: true, description: "界面显示名(如「傲娇」)" },
|
|
67
|
+
description: { type: "string", required: true, description: "一句话简介" },
|
|
68
|
+
promptText: { type: "string", required: true, description: "完整风格契约:称呼/emoji/语气词/节奏/立场,与内置契约同构" },
|
|
69
|
+
},
|
|
70
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
|
|
71
|
+
execute: async (args) => {
|
|
72
|
+
if (!deps.identity)
|
|
73
|
+
throw new Error("lume deps.identity store is unavailable");
|
|
74
|
+
await deps.identity.setCustomPersona(String(args.name), {
|
|
75
|
+
displayName: String(args.displayName),
|
|
76
|
+
description: String(args.description ?? ""),
|
|
77
|
+
promptText: String(args.promptText),
|
|
78
|
+
createdAt: Date.now(),
|
|
79
|
+
});
|
|
80
|
+
return { ok: true };
|
|
81
|
+
},
|
|
82
|
+
}));
|
|
83
|
+
}, "lume: persona tools");
|
|
84
|
+
// ── 任务载具工具(第二组写入通道)──
|
|
85
|
+
// 与人格工具一样是「模型主动调用、零额外 LLM 调用」,区别在写入对象:契约/台账/假设
|
|
86
|
+
// 属于当前任务(会话态),项目知识按工作目录跨会话累积。列表类参数统一用字符串
|
|
87
|
+
// 分隔(分号或换行),不引入数组 schema——省 schema token,也少一层校验风险。
|
|
88
|
+
const splitList = (value) => String(value ?? "")
|
|
89
|
+
.split(/[;;\n]/)
|
|
90
|
+
.map((item) => item.trim())
|
|
91
|
+
.filter(Boolean);
|
|
92
|
+
deps.ctx.effect(() => {
|
|
93
|
+
deps.ctx.tools.register(defineTool({
|
|
94
|
+
name: "lume_contract",
|
|
95
|
+
description: "写下或更新本任务的任务契约(需求量化的落点):目标、范围、数量、完成判据、非目标、待确认。任务型请求开工前调用一次;探索后回填实际数量;之后只传变化的字段即可(局部更新)。",
|
|
96
|
+
parameters: {
|
|
97
|
+
goal: { type: "string", description: "目标:一句话、可观察的结果" },
|
|
98
|
+
scope: { type: "string", description: "范围:路径/模块/章节,分号或换行分隔" },
|
|
99
|
+
expectCount: { type: "number", required: true, description: "预计数量(探索前先估)" },
|
|
100
|
+
actualCount: { type: "number", description: "实际数量(探索后回填)" },
|
|
101
|
+
criteria: { type: "string", description: "完成判据:可执行、可核对,分号或换行分隔" },
|
|
102
|
+
nonGoals: { type: "string", description: "非目标:明确不动的东西,分号分隔" },
|
|
103
|
+
open: { type: "string", description: "待确认:只列真正阻塞的(≤2 个),分号分隔" },
|
|
104
|
+
},
|
|
105
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记录任务契约" }] },
|
|
106
|
+
execute: async (args, exec) => {
|
|
107
|
+
if (!deps.projectOf())
|
|
108
|
+
throw new Error("lume deps.projectOf() store is unavailable");
|
|
109
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
110
|
+
if (!sid)
|
|
111
|
+
throw new Error("lume_contract requires an active session");
|
|
112
|
+
const st = deps.runtime.get(sid);
|
|
113
|
+
const normalized = deps.normalizeContract({
|
|
114
|
+
goal: args.goal,
|
|
115
|
+
scope: splitList(args.scope),
|
|
116
|
+
expectCount: args.expectCount,
|
|
117
|
+
actualCount: args.actualCount,
|
|
118
|
+
criteria: splitList(args.criteria),
|
|
119
|
+
nonGoals: splitList(args.nonGoals),
|
|
120
|
+
open: splitList(args.open),
|
|
121
|
+
}, Date.now(), st.turnIndex);
|
|
122
|
+
// 项目域可能还没兑现(异步):统一走取用器,不可用时给出可读错误
|
|
123
|
+
const store = deps.projectStore();
|
|
124
|
+
const existing = store.getContract(sid);
|
|
125
|
+
if (existing) {
|
|
126
|
+
// 局部更新:未传的字段保持原值(回填数量时不该把判据清空)。
|
|
127
|
+
const patch = {};
|
|
128
|
+
if (args.goal !== undefined)
|
|
129
|
+
patch.goal = normalized.goal;
|
|
130
|
+
if (args.scope !== undefined)
|
|
131
|
+
patch.scope = normalized.scope;
|
|
132
|
+
if (args.expectCount !== undefined)
|
|
133
|
+
patch.expectCount = normalized.expectCount;
|
|
134
|
+
if (args.actualCount !== undefined)
|
|
135
|
+
patch.actualCount = normalized.actualCount;
|
|
136
|
+
if (args.criteria !== undefined)
|
|
137
|
+
patch.criteria = normalized.criteria;
|
|
138
|
+
if (args.nonGoals !== undefined)
|
|
139
|
+
patch.nonGoals = normalized.nonGoals;
|
|
140
|
+
if (args.open !== undefined)
|
|
141
|
+
patch.open = normalized.open;
|
|
142
|
+
await store.patchContract(sid, patch);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
if (!normalized.goal)
|
|
146
|
+
throw new Error("lume_contract requires a goal on first write");
|
|
147
|
+
await store.setContract(sid, normalized);
|
|
148
|
+
}
|
|
149
|
+
return { ok: true };
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
deps.ctx.tools.register(defineTool({
|
|
153
|
+
name: "lume_change",
|
|
154
|
+
description: "改动台账:记录/更新一处将要改或已改的位置(文件/符号/文档章节 → 改什么 → 怎么验 → 状态)。动手前先列计划项,改完推进状态;只推进状态时可只传 target + status。文档任务用章节名当 target,形成分节记账。",
|
|
155
|
+
parameters: {
|
|
156
|
+
target: { type: "string", required: true, description: "目标位置:文件路径 / 符号 / 文档章节" },
|
|
157
|
+
change: { type: "string", description: "改什么(一句话)" },
|
|
158
|
+
why: { type: "string", description: "为什么改(对齐契约的哪一条)" },
|
|
159
|
+
verify: { type: "string", description: "怎么验(命令 / 回读 / 对照)" },
|
|
160
|
+
status: { type: "string", description: "planned | done | verified | skipped" },
|
|
161
|
+
},
|
|
162
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新改动台账" }] },
|
|
163
|
+
execute: async (args, exec) => {
|
|
164
|
+
if (!deps.projectOf())
|
|
165
|
+
throw new Error("lume deps.projectOf() store is unavailable");
|
|
166
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
167
|
+
if (!sid)
|
|
168
|
+
throw new Error("lume_change requires an active session");
|
|
169
|
+
const target = String(args.target ?? "").trim();
|
|
170
|
+
if (!target)
|
|
171
|
+
throw new Error("lume_change requires a target");
|
|
172
|
+
const status = args.status;
|
|
173
|
+
const allowed = status === "planned" || status === "done" || status === "verified" || status === "skipped" ? status : undefined;
|
|
174
|
+
if (args.change === undefined && allowed !== undefined) {
|
|
175
|
+
const hit = await deps.projectStore().setChangeStatus(sid, target, allowed);
|
|
176
|
+
if (!hit)
|
|
177
|
+
throw new Error(`lume_change: no ledger entry for ${target}`);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
}
|
|
180
|
+
const item = deps.normalizeChange({ target, change: args.change, why: args.why, verify: args.verify, status: allowed }, Date.now());
|
|
181
|
+
if (!item)
|
|
182
|
+
throw new Error("lume_change requires target and change");
|
|
183
|
+
await deps.projectStore().upsertChange(sid, item);
|
|
184
|
+
return { ok: true };
|
|
185
|
+
},
|
|
186
|
+
}));
|
|
187
|
+
deps.ctx.tools.register(defineTool({
|
|
188
|
+
name: "lume_hypothesis",
|
|
189
|
+
description: "假设台账:记录一条正在验证的假设及其证据与状态(open/testing/confirmed/excluded)。排查类任务里每验证一次就更新状态;已排除的假设不要再重复尝试。**下结论(confirmed/excluded)就是一次裁决**:证据、裁决方式、反例检查三项必填,缺一会被拒。",
|
|
190
|
+
parameters: {
|
|
191
|
+
text: { type: "string", required: true, description: "假设内容,一句话" },
|
|
192
|
+
evidence: { type: "string", description: "支持或推翻它的观察(含命令输出 / 文件行 / 时间戳)" },
|
|
193
|
+
status: { type: "string", description: "open | testing | confirmed | excluded" },
|
|
194
|
+
method: { type: "string", description: "裁决方式:用哪条命令 / 工具、看什么结果判定的(confirmed/excluded 必填)" },
|
|
195
|
+
counter: { type: "string", description: "反例检查:找过哪些反例,或「已尝试 X 未找到反例」(confirmed/excluded 必填)" },
|
|
196
|
+
},
|
|
197
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新假设台账" }] },
|
|
198
|
+
execute: async (args, exec) => {
|
|
199
|
+
if (!deps.projectOf())
|
|
200
|
+
throw new Error("lume deps.projectOf() store is unavailable");
|
|
201
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
202
|
+
if (!sid)
|
|
203
|
+
throw new Error("lume_hypothesis requires an active session");
|
|
204
|
+
const st = deps.runtime.get(sid);
|
|
205
|
+
const status = String(args.status ?? "open").trim();
|
|
206
|
+
const evidence = String(args.evidence ?? "").trim();
|
|
207
|
+
const method = String(args.method ?? "").trim();
|
|
208
|
+
const counter = String(args.counter ?? "").trim();
|
|
209
|
+
// 结论级裁决门禁(机械可判,不比模型自评):下结论 = 裁决,三样缺一不可。
|
|
210
|
+
// 为什么要硬拦:真机里「confirmed/excluded」曾经只带一句「已验证 / 无」,
|
|
211
|
+
// 那种结论无法复核,等于把猜测固化成跨轮次的事实。
|
|
212
|
+
let stored = evidence;
|
|
213
|
+
if (status === "confirmed" || status === "excluded") {
|
|
214
|
+
if (evidence.length < 8)
|
|
215
|
+
throw new Error("下结论必须带证据:evidence 写清观察(命令输出 / 文件行 / 时间戳),不要只写「已验证」");
|
|
216
|
+
if (method.length < 6)
|
|
217
|
+
throw new Error("下结论必须写裁决方式 method:用哪条命令 / 工具、看什么结果判定的(例:npm test 全绿 / grep 到 X)");
|
|
218
|
+
if (counter.length < 8)
|
|
219
|
+
throw new Error("下结论必须写反例检查 counter:找过哪些反例,或「已尝试 X 未找到反例」——只写「无」不算");
|
|
220
|
+
// 证据里的文件引用必须真的看过(与引用核对同一套 pathKey 口径),否则就是编证据
|
|
221
|
+
// 只把「像路径」的 token 当引用:含分隔符或带 :行号。否则 Node.js / e.g / i.e 都会被
|
|
222
|
+
// 当成文件引用,写一句诚实的证据反而被拒(外部审核指出)。
|
|
223
|
+
const refs = [...evidence.matchAll(/([A-Za-z0-9_./\\-]+\.[A-Za-z]{1,5})(:\d+)?/g)]
|
|
224
|
+
.filter((m) => (m[0] ?? "").includes("/") || (m[0] ?? "").includes("\\") || Boolean(m[2]))
|
|
225
|
+
.map((m) => m[1] ?? "")
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
const unseen = [...new Set(refs)].filter((ref) => !st.agent.evidence.has(pathKey(ref)));
|
|
228
|
+
if (unseen.length > 0)
|
|
229
|
+
throw new Error(`证据里有本会话没看过的引用:${unseen.join("、")}——先去看过再引用(引用必须能被核对)`);
|
|
230
|
+
// 结构化保存三件套(早先折进 evidence 一行会让「多少结论带反例检查」没法机械统计,
|
|
231
|
+
// 也会在改回 open/testing 时被新证据覆盖)
|
|
232
|
+
stored = evidence;
|
|
233
|
+
}
|
|
234
|
+
const item = deps.normalizeHypothesis({ text: args.text, evidence: stored, status, method, counter }, Date.now());
|
|
235
|
+
if (!item)
|
|
236
|
+
throw new Error("lume_hypothesis requires text");
|
|
237
|
+
await deps.projectStore().upsertHypothesis(sid, item);
|
|
238
|
+
deps.runtime.get(sid).hypothesesTouched = true;
|
|
239
|
+
return { ok: true };
|
|
240
|
+
},
|
|
241
|
+
}));
|
|
242
|
+
deps.ctx.tools.register(defineTool({
|
|
243
|
+
name: "lume_project_note",
|
|
244
|
+
description: "记录一条**稳定的项目事实**(按工作目录跨会话累积):构建/测试命令、模块数据流、仓库约定、或一条死路(试过但行不通的做法)。只记可复用、已验证的事实,不要记一次性进展。",
|
|
245
|
+
parameters: {
|
|
246
|
+
kind: { type: "string", required: true, description: "build | test | module | convention | deadend" },
|
|
247
|
+
text: { type: "string", required: true, description: "事实本身,一句话,≤200 字" },
|
|
248
|
+
},
|
|
249
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记入项目知识" }] },
|
|
250
|
+
execute: async (args, exec) => {
|
|
251
|
+
if (!deps.projectOf())
|
|
252
|
+
throw new Error("lume deps.projectOf() store is unavailable");
|
|
253
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
254
|
+
if (!sid)
|
|
255
|
+
throw new Error("lume_project_note requires an active session");
|
|
256
|
+
const fact = deps.normalizeProjectFact({ kind: args.kind, text: args.text }, Date.now(), {
|
|
257
|
+
taskTitle: deps.runtime.get(sid).sessionTitle,
|
|
258
|
+
requirementHints: deps.requirementHintsOf(deps.runtime.get(sid).cwd),
|
|
259
|
+
});
|
|
260
|
+
if (!fact)
|
|
261
|
+
throw new Error("lume_project_note requires text");
|
|
262
|
+
if (deps.looksSensitive(fact.text))
|
|
263
|
+
throw new Error("lume_project_note 拒绝含密钥/连接串/凭证的内容:项目知识是**明文跨会话**存储;请改记「存在某类配置,细节见 <文件:行>」");
|
|
264
|
+
const projectKey = deps.projectKeyFor(sid, { agent: exec?.agent });
|
|
265
|
+
if (!projectKey) {
|
|
266
|
+
// 拿不到工作目录时**暂存**而不是丢弃——现场代价:模型主动记的 3 条硬知识全丢了。
|
|
267
|
+
// 仍然不写跨会话表:写一次就会把不同项目的知识串进同一个键(现场事故:facts 的键曾是 "unknown")。
|
|
268
|
+
const rt = deps.runtime.get(sid);
|
|
269
|
+
rt.pendingFacts.push(fact);
|
|
270
|
+
if (rt.pendingFacts.length > 8)
|
|
271
|
+
rt.pendingFacts.shift();
|
|
272
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 项目知识已暂存(工作目录未知,共 ${rt.pendingFacts.length} 条),拿到 cwd 后补落盘`);
|
|
273
|
+
return { ok: true };
|
|
274
|
+
}
|
|
275
|
+
await deps
|
|
276
|
+
.projectStore()
|
|
277
|
+
.addFact(projectKey, fact, (candidate, existing) => existing.some((entry) => deps.jaccard(entry.text, candidate) >= 0.7));
|
|
278
|
+
return { ok: true };
|
|
279
|
+
},
|
|
280
|
+
}));
|
|
281
|
+
deps.ctx.tools.register(defineTool({
|
|
282
|
+
name: "lume_project_forget",
|
|
283
|
+
description: "删掉一条已过时/记错的项目知识(注入块里的 #编号 或短 id)。旧结论被推翻时用它,别让错误知识继续跨会话传播。",
|
|
284
|
+
parameters: {
|
|
285
|
+
id: { type: "string", required: true, description: "要删的条目引用(#7 或 7 或短 id,见项目知识块里的 #编号·短id)" },
|
|
286
|
+
},
|
|
287
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已删除该条项目知识" }] },
|
|
288
|
+
execute: async (args, exec) => {
|
|
289
|
+
if (!deps.projectOf())
|
|
290
|
+
throw new Error("lume deps.projectOf() store is unavailable");
|
|
291
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
292
|
+
if (!sid)
|
|
293
|
+
throw new Error("lume_project_forget requires an active session");
|
|
294
|
+
const ref = String(args.id ?? "").trim();
|
|
295
|
+
if (!ref)
|
|
296
|
+
throw new Error("lume_project_forget requires id");
|
|
297
|
+
const projectKey = deps.projectKeyFor(sid, { agent: exec?.agent });
|
|
298
|
+
if (!projectKey)
|
|
299
|
+
throw new Error("lume_project_forget 拿不到工作目录,无法定位知识库");
|
|
300
|
+
const removed = await deps.projectStore().deleteFactById(projectKey, ref);
|
|
301
|
+
if (!removed)
|
|
302
|
+
throw new Error(`lume_project_forget 没找到 ${ref}(用项目知识块里的 #编号 或短 id)`);
|
|
303
|
+
return { ok: true };
|
|
304
|
+
},
|
|
305
|
+
}));
|
|
306
|
+
deps.ctx.tools.register(defineTool({
|
|
307
|
+
name: "lume_design",
|
|
308
|
+
description: "记一条设计决策(功能型任务的设计 pass):决策点 → 选择 → 被放弃的方案与理由 → 影响面。新增字段/接口/页面这类需求,动手前先写;写下后会跨轮回显,交付时按它对账。",
|
|
309
|
+
parameters: {
|
|
310
|
+
point: { type: "string", required: true, description: "决策点:例如「权限人字段存在哪」" },
|
|
311
|
+
choice: { type: "string", required: true, description: "定下来的做法(一句话)" },
|
|
312
|
+
rejected: { type: "string", description: "被放弃的方案与理由(没有它就是没做取舍)" },
|
|
313
|
+
impact: { type: "string", description: "影响面:会经过哪些既有路径(其它 tab/导出/导入/报表/外部同步)" },
|
|
314
|
+
},
|
|
315
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记录设计决策" }] },
|
|
316
|
+
execute: async (args, exec) => {
|
|
317
|
+
if (!deps.projectOf())
|
|
318
|
+
throw new Error("lume deps.projectOf() store is unavailable");
|
|
319
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
320
|
+
if (!sid)
|
|
321
|
+
throw new Error("lume_design requires an active session");
|
|
322
|
+
const item = deps.normalizeDesign({ point: args.point, choice: args.choice, rejected: args.rejected, impact: args.impact }, Date.now());
|
|
323
|
+
if (!item)
|
|
324
|
+
throw new Error("lume_design requires point and choice");
|
|
325
|
+
await deps.projectStore().upsertDesign(sid, item);
|
|
326
|
+
return { ok: true };
|
|
327
|
+
},
|
|
328
|
+
}));
|
|
329
|
+
}, "lume: carrier tools");
|
|
330
|
+
// ── 度量自读(0.8.x)──
|
|
331
|
+
// 让「是不是更聪明了」可以被查,而不是靠印象:只回机械事实(路由判定与命中判据、
|
|
332
|
+
// 用户纠正/重复请求/越权改动、块装配预算、触发器命中后行为是否变化),不做语义解释,
|
|
333
|
+
// 测不了的项目会注明「无机械口径」。
|
|
334
|
+
//
|
|
335
|
+
// **单开一个 effect,并且整段 try/catch**:它是本插件唯一一个自定义输出 schema 的工具
|
|
336
|
+
// (带 text 字段 + 动态 render)。注册期一旦抛错,会像 0.7.1 的 RPC 注册那样把整段
|
|
337
|
+
// apply 打断(那次界面上连人设都看不到)。度量是**观测设施**,缺了不该影响对话本身。
|
|
338
|
+
deps.ctx.effect(() => {
|
|
339
|
+
try {
|
|
340
|
+
deps.ctx.tools.register(defineTool({
|
|
341
|
+
name: "lume_metrics",
|
|
342
|
+
description: "读取 Lume 的运行时度量:最近的路由判定与命中的判据、用户纠正/重复请求/越权改动等外部结果信号、块装配预算、每个触发器命中后行为是否真的变了。用户问「最近效果如何」「为什么这轮判成问答」时用它回答,不要凭印象编。",
|
|
343
|
+
parameters: {
|
|
344
|
+
scope: { type: "string", description: "session(默认)= 本会话;all = 本落点全部会话" },
|
|
345
|
+
},
|
|
346
|
+
output: {
|
|
347
|
+
schema: METRICS_OUTPUT_SCHEMA,
|
|
348
|
+
render: (_args, value) => [
|
|
349
|
+
{ type: "text", text: String(value?.text ?? "") },
|
|
350
|
+
],
|
|
351
|
+
},
|
|
352
|
+
execute: async (args, exec) => {
|
|
353
|
+
const scope = String(args.scope ?? "session")
|
|
354
|
+
.trim()
|
|
355
|
+
.toLowerCase();
|
|
356
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
357
|
+
const text = scope === "all" ? deps.metricsSummary() : deps.metricsSummary(sid);
|
|
358
|
+
return { ok: true, text };
|
|
359
|
+
},
|
|
360
|
+
}));
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
// 注册失败只有一条后果:查不到度量。如实留痕,但不影响对话与其它工具。
|
|
364
|
+
deps.ctx.logger?.warn?.("lume: lume_metrics 注册失败(度量是观测设施,缺它不影响对话)", error);
|
|
365
|
+
}
|
|
366
|
+
}, "lume: metrics tool");
|
|
367
|
+
}
|
package/lib/host/triggers.js
CHANGED
|
@@ -12,7 +12,16 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { deadPathKind } from "../core/signals.js";
|
|
14
14
|
export function newTriggerCounters() {
|
|
15
|
-
return {
|
|
15
|
+
return {
|
|
16
|
+
inspectStreak: 0,
|
|
17
|
+
mutateStreak: 0,
|
|
18
|
+
verifyFailStreak: 0,
|
|
19
|
+
verifyEnvHits: 0,
|
|
20
|
+
mutations: 0,
|
|
21
|
+
steps: 0,
|
|
22
|
+
codeInspects: 0,
|
|
23
|
+
unfoundedChanges: 0,
|
|
24
|
+
};
|
|
16
25
|
}
|
|
17
26
|
/** 计数器推进:语义是「行为模式」,因此只在类别切换或验证成败时重置。 */
|
|
18
27
|
export function applyToolSignal(counters, kind, signals) {
|
|
@@ -61,6 +70,8 @@ export const DEFAULT_TRIGGER_THRESHOLDS = {
|
|
|
61
70
|
changeStreak: 4,
|
|
62
71
|
deadPathFails: 3,
|
|
63
72
|
knowledgeSteps: 20,
|
|
73
|
+
// 6:实测这类型任务的轨迹是「grep/read 十几轮 → 直接动手」,6 次摸到代码就是该做设计的时点
|
|
74
|
+
designAfterInspects: 6,
|
|
64
75
|
};
|
|
65
76
|
/**
|
|
66
77
|
* 工具事件触发的判定。一次只返回**一个**(按紧急度排序):同时堆三条提醒会互相稀释。
|
|
@@ -82,6 +93,13 @@ export function evaluateToolTrigger(counters, ctx, thresholds = DEFAULT_TRIGGER_
|
|
|
82
93
|
text: `〔死路提醒〕同一验证已连续失败 ${n} 次。先归因(输入 / 逻辑 / 接口 / 环境 / 权限),把结论写进假设台账:证实的标 confirmed、排除的标 excluded(用 lume_hypothesis)——已排除的假设不要再试。换一个方案再动手。`,
|
|
83
94
|
};
|
|
84
95
|
}
|
|
96
|
+
// 决策分档:正在拿推测当依据改东西。排在「改多了要验证」之前——因为这一步的**依据**有问题。
|
|
97
|
+
if (counters.unfoundedChanges > 0 && ctx.blindTarget) {
|
|
98
|
+
return {
|
|
99
|
+
id: "unfounded-change",
|
|
100
|
+
text: `〔决策分档〕这一步在改一个**本会话没读过**的目标:${ctx.blindTarget}。二选一:① 先做最便宜的核实(读一次 / 跑一次 / 查一次引用),拿到事实再改;② 确实要带着不确定动手 → 用 lume_hypothesis 把它落成假设(写清证据与「怎么验证」,并说明「如果这条不成立会怎样」)。不要拿推测当依据直接改。`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
85
103
|
if (counters.mutateStreak >= thresholds.changeStreak || ctx.unverifiedChanges >= thresholds.changeStreak) {
|
|
86
104
|
return {
|
|
87
105
|
id: "verify-as-you-go",
|
|
@@ -94,6 +112,12 @@ export function evaluateToolTrigger(counters, ctx, thresholds = DEFAULT_TRIGGER_
|
|
|
94
112
|
text: "〔载具缺失〕你已经动手改动,但还没写下任务契约。花一次调用写清:目标(可观察的结果)、范围(精确到路径/模块/章节)、预计数量、完成判据(可执行)、非目标(明确不动什么)、待确认(≤2 个)。之后每步以契约为准,交付时按它逐项对账——用 lume_contract。",
|
|
95
113
|
};
|
|
96
114
|
}
|
|
115
|
+
if (ctx.isTask && ctx.designSignal && !ctx.hasDesign && counters.codeInspects >= thresholds.designAfterInspects) {
|
|
116
|
+
return {
|
|
117
|
+
id: "design-missing",
|
|
118
|
+
text: `〔设计缺失〕你已经读了 ${counters.codeInspects} 处代码,但还没有一条设计决策。功能型任务最容易在这里翻车:直接照需求改代码,把「数据落在哪 / 接口长什么样 / 照哪个既有范式」留给临场发挥。现在花一次调用写进 lume_design(point / choice / rejected / impact 各一行),写完再动手。`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
97
121
|
if (ctx.isTask && counters.inspectStreak >= thresholds.inspectStreak) {
|
|
98
122
|
return {
|
|
99
123
|
id: "converge",
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { claimsVerification, looksLikeFailure } from "../core/signals.js";
|
|
2
|
+
export function handleTurnEnd(deps, sid, st, session) {
|
|
3
|
+
st.turnIndex++;
|
|
4
|
+
// 轮边界补一次项目知识落盘:此时 cwd 通常已经从提示词上下文拿到
|
|
5
|
+
deps.flushPendingFacts(sid, session);
|
|
6
|
+
// 切换窗口的消耗只发生在轮边界(渲染函数只读状态,不再就地清零):
|
|
7
|
+
// 同一步里 prompt 会被构建多次,若在渲染里消耗窗口,第二次构建就会
|
|
8
|
+
// 丢掉接班招呼——那是「注入随构建次数漂移」,正是本版要消灭的东西。
|
|
9
|
+
st.switchGreetingPending = false;
|
|
10
|
+
if (st.switchTurn !== null && st.turnIndex - st.switchTurn >= deps.boundaryTurns)
|
|
11
|
+
st.switchTurn = null;
|
|
12
|
+
// ── 行为触发器(轮边界)──
|
|
13
|
+
// 连击按轮清零:新一轮是新请求,上一轮的「撒网」不该继续累加;死路连击
|
|
14
|
+
// 跨轮保留(同一环境不可用是会话级事实)。上轮的提醒到这里失效。
|
|
15
|
+
st.triggerCounters.inspectStreak = 0;
|
|
16
|
+
st.triggerCounters.mutateStreak = 0;
|
|
17
|
+
deps.clearNotice(st, "trigger");
|
|
18
|
+
deps.forceNotice(st, "turn", null);
|
|
19
|
+
st.hypothesesTouched = false;
|
|
20
|
+
if (deps.behaviorTriggersOn && deps.projectOf()) {
|
|
21
|
+
const fire = deps.evaluateTurnTrigger({
|
|
22
|
+
turnIndex: st.turnIndex,
|
|
23
|
+
hasContract: deps.contractOf(sid) !== null,
|
|
24
|
+
compactionTurn: st.compaction?.turnIndex ?? null,
|
|
25
|
+
lastDriftTurn: st.lastDriftTurn,
|
|
26
|
+
counters: st.triggerCounters,
|
|
27
|
+
knowledgePrompted: st.knowledgePrompted,
|
|
28
|
+
}, deps.triggerThresholds);
|
|
29
|
+
if (fire && deps.cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
30
|
+
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
31
|
+
deps.recordMetric({
|
|
32
|
+
kind: "trigger",
|
|
33
|
+
at: Date.now(),
|
|
34
|
+
sid,
|
|
35
|
+
turn: st.turnIndex,
|
|
36
|
+
id: fire.id,
|
|
37
|
+
counters: { ...st.triggerCounters },
|
|
38
|
+
});
|
|
39
|
+
if (fire.id === "criteria-drift") {
|
|
40
|
+
// 契约对账用「交付口径」渲染原始判据:防判据随进展漂移。
|
|
41
|
+
st.lastDriftTurn = st.turnIndex;
|
|
42
|
+
deps.forceNotice(st, "turn", deps.renderContract(deps.contractOf(sid), true));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
st.knowledgePrompted = true;
|
|
46
|
+
deps.forceNotice(st, "turn", fire.text);
|
|
47
|
+
}
|
|
48
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 轮触发器 ${fire.id}(turn=${st.turnIndex})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 低成本会话内纠偏:只处理明确的错误/失败信号,且要求连续轮次用户请求相同。
|
|
52
|
+
const failed = looksLikeFailure(st.assistantText);
|
|
53
|
+
const queryKey = st.userText.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
54
|
+
if (failed && queryKey && queryKey === st.lastFailureQuery)
|
|
55
|
+
st.failureStreak++;
|
|
56
|
+
else if (failed && queryKey) {
|
|
57
|
+
st.lastFailureQuery = queryKey;
|
|
58
|
+
st.failureStreak = 1;
|
|
59
|
+
}
|
|
60
|
+
else if (!failed) {
|
|
61
|
+
st.failureStreak = 0;
|
|
62
|
+
st.lastFailureQuery = null;
|
|
63
|
+
deps.forceNotice(st, "protocol", null);
|
|
64
|
+
}
|
|
65
|
+
if (st.failureStreak >= 2)
|
|
66
|
+
deps.forceNotice(st, "protocol", "检测到相同请求连续失败:先定位根因并记录已排除假设,再选择不同方案;不要重复同一调用。");
|
|
67
|
+
// 空转信号:判成执行/诊断,但这一轮一次工具都没调——要么在纸上谈兵,要么就是路由判错了。
|
|
68
|
+
// 与「越权改动」凑成一对:一个抓「该动没动」,一个抓「不该动却动了」。
|
|
69
|
+
if ((st.interactionMode === "execute" || st.interactionMode === "diagnosis") && (st.toolCalls ?? 0) === 0)
|
|
70
|
+
deps.recordMetric({
|
|
71
|
+
kind: "outcome",
|
|
72
|
+
at: Date.now(),
|
|
73
|
+
sid,
|
|
74
|
+
turn: st.turnIndex,
|
|
75
|
+
event: "no-action",
|
|
76
|
+
mode: st.interactionMode,
|
|
77
|
+
detail: st.intent?.text?.slice(0, 120) ?? "",
|
|
78
|
+
});
|
|
79
|
+
const claimsVerificationNow = claimsVerification(st.assistantText);
|
|
80
|
+
// 交付对账(C4):台账里还有"已改未验"就**列出具体条目**——泛泛提醒"要有验证证据"
|
|
81
|
+
// 实测没用,摆出未验证的具体项才有可执行性。
|
|
82
|
+
const deliveryNotice = st.interactionMode === "execute" || st.triggerCounters.mutations > 0 ? deps.buildUnverifiedDeliveryNotice(deps.changesOf(sid)) : null;
|
|
83
|
+
// 载具缺口:动了代码但契约/设计都空 → 交付时如实说(触发器喊过没用,只能靠事实)
|
|
84
|
+
const carrierGap = deps.noticeOpen(st, "carrierGap")
|
|
85
|
+
? deps.buildCarrierGapNotice({
|
|
86
|
+
mutations: st.triggerCounters.mutations,
|
|
87
|
+
hasContract: deps.contractOf(sid) !== null,
|
|
88
|
+
hasDesign: deps.designOf(sid).length > 0,
|
|
89
|
+
})
|
|
90
|
+
: null;
|
|
91
|
+
deps.setNotice(st, "carrierGap", carrierGap);
|
|
92
|
+
deps.forceNotice(st, "postTurn", [deliveryNotice, carrierGap].filter(Boolean).join("\n\n") ||
|
|
93
|
+
(st.interactionMode === "execute" && st.assistantText && !claimsVerificationNow
|
|
94
|
+
? "〔上轮交付复核〕上一轮执行回复没有给出可见的验证证据。本轮若继续处理同一任务,先确认上轮变更是否真实生效,再继续扩大范围。"
|
|
95
|
+
: null));
|
|
96
|
+
if (st.interactionMode === "execute")
|
|
97
|
+
st.taskPhase = deps.advancePhase(st.taskPhase, st.toolFailures > 0 || st.toolUnknown > 0 ? "diagnose" : claimsVerificationNow ? "deliver" : "verify");
|
|
98
|
+
// 即时对齐只影响当前轮;下一轮重新根据用户消息判断,避免纠偏条款滞留。
|
|
99
|
+
deps.clearNotice(st, "align");
|
|
100
|
+
// 风格泄漏检测挂在 turn/end(该事件已被窗口机制验证可靠;assistant/message
|
|
101
|
+
// 的投递在实测中不可靠)。切换完成后逐轮检查回复是否残留旧人设签名词,
|
|
102
|
+
// 窗口已关仍检出 → 重开窗口 + 升级播报;一轮干净回复自动解除升级。
|
|
103
|
+
if (st.prevSignatures.length > 0 && st.lastInjected !== undefined && st.assistantText) {
|
|
104
|
+
const report = deps.detectLeak(st.assistantText, st.prevSignatures);
|
|
105
|
+
const inWindow = st.switchTurn !== null && st.turnIndex - st.switchTurn < deps.boundaryTurns;
|
|
106
|
+
if (report.leaked && !inWindow) {
|
|
107
|
+
st.switchTurn = st.turnIndex;
|
|
108
|
+
st.leakEscalated = true;
|
|
109
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 检测到旧人设风格泄漏(${report.hits.map((h) => `${h.word}×${h.count}`).join("、")}),重新注入升级版切换播报`);
|
|
110
|
+
}
|
|
111
|
+
else if (!report.leaked) {
|
|
112
|
+
st.leakEscalated = false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
deps.scheduleExtraction(sid, st);
|
|
116
|
+
}
|