lume-dsh-plugin 0.7.3 → 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 +129 -18
- 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 +182 -11
- 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 +144 -11
- 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 +18 -5
- package/lib/host/thinking.js +10 -1
- package/lib/host/tools.js +367 -0
- package/lib/host/triggers.js +30 -6
- 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 +334 -836
- package/package.json +13 -4
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { pathKey } from "../core/citations.js";
|
|
4
|
+
import { claimsVerification, looksLikeFailure } from "../core/signals.js";
|
|
5
|
+
import { handleTurnEnd } from "./turn-boundary.js";
|
|
6
|
+
import { AUTO_FACT_CAP, anchorRequirement, applyAlignNotice, checkContextPressure, checkEvidenceNotices, checkRequirementDrift, collectAssistantFacts, collectUserRuleFacts, handleCompactionCheckpoint, learnWorkspaceFromSnapshot, } from "./inbound.js";
|
|
7
|
+
/** 每会话自动沉淀的项目知识上限:宁可少记,也不要让知识库变垃圾桶。 */
|
|
8
|
+
/**
|
|
9
|
+
* 「本会话看过这个目标吗」的**唯一**判据:证据索引(pathKey 归一)+ 摸过的目标。
|
|
10
|
+
* 首改定位门槛与决策分档共用它——同一种问题只能有一个真值来源(两处各用一种键会得出两种结论)。
|
|
11
|
+
*/
|
|
12
|
+
function inspectedTarget(st, target) {
|
|
13
|
+
const key = pathKey(target);
|
|
14
|
+
if (st.agent.evidence.has(key))
|
|
15
|
+
return true;
|
|
16
|
+
for (const seen of st.agent.inspectedTargets)
|
|
17
|
+
if (pathKey(seen) === key)
|
|
18
|
+
return true;
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 只对**已存在**的文件判「没读过就改」:新建文件天然没读过,对它喊「先读一次」是错话。
|
|
23
|
+
* 相对路径按会话 cwd 解析;解析不了(cwd 未知)就当新建处理——宁可漏,不喊错话。
|
|
24
|
+
*/
|
|
25
|
+
function existingTarget(st, target) {
|
|
26
|
+
const raw = String(target ?? "").trim();
|
|
27
|
+
if (!raw)
|
|
28
|
+
return null;
|
|
29
|
+
const resolved = isAbsolute(raw) ? raw : st.cwd ? join(st.cwd, raw) : raw;
|
|
30
|
+
return existsSync(resolved) ? raw : null;
|
|
31
|
+
}
|
|
32
|
+
export function createSessionEventHandler(deps) {
|
|
33
|
+
return (session, event) => {
|
|
34
|
+
const sid = String(session.id);
|
|
35
|
+
const st = deps.runtime.get(sid);
|
|
36
|
+
switch (event.type) {
|
|
37
|
+
case "request/context": {
|
|
38
|
+
// 路由缓存的真正来源:agent-loop 在路由变化时 append 的 request/context
|
|
39
|
+
// ({provider, model, contextWindow})。request/header 的载荷是 {header,
|
|
40
|
+
// reason},拿不到 provider/model——v0.3.0 一直监听错了事件,提取从未跑通。
|
|
41
|
+
const data = event.data;
|
|
42
|
+
// 上下文窗口:预警要用(0/未知就不预警)
|
|
43
|
+
st.contextWindow = Number(data?.contextWindow) || st.contextWindow;
|
|
44
|
+
if (typeof data?.provider === "string" && typeof data?.model === "string") {
|
|
45
|
+
deps.llmRoute.current = { provider: data.provider, model: data.model };
|
|
46
|
+
deps.ctx.logger?.warn?.(`lume: request/context 更新 deps.llmRoute → ${deps.llmRoute.current?.provider}/${deps.llmRoute.current?.model}`);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
deps.ctx.logger?.warn?.("lume: request/context 未携带 provider/model,保留 deps.llmRoute", data);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case "session/title": {
|
|
54
|
+
// 会话标题:会话记忆的标识 + 新会话的「继续 X」指令依赖它。
|
|
55
|
+
st.sessionTitle = String(event.data?.title ?? "").slice(0, 60);
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
case "user/message": {
|
|
59
|
+
if (handleCompactionCheckpoint(sid, st, deps, event.data))
|
|
60
|
+
break;
|
|
61
|
+
learnWorkspaceFromSnapshot(sid, st, deps, event.data);
|
|
62
|
+
if (!deps.isUserAuthored(event.data))
|
|
63
|
+
break;
|
|
64
|
+
const text = deps.messageText(event.data);
|
|
65
|
+
if (!text)
|
|
66
|
+
break;
|
|
67
|
+
const normalized = text.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
68
|
+
const explicitCorrection = /不是这个意思|不是我说的|你理解错|答非所问|听不懂|我说的是|我指的是|不对|错了|别这样|重新来/i.test(text);
|
|
69
|
+
const repeatedRequest = normalized.length >= 5 && st.recentUserQueries.includes(normalized);
|
|
70
|
+
st.userText = text;
|
|
71
|
+
anchorRequirement(sid, st, deps, text);
|
|
72
|
+
collectUserRuleFacts(sid, st, deps, text, event.data);
|
|
73
|
+
applyAlignNotice(st, deps, explicitCorrection, repeatedRequest);
|
|
74
|
+
// 外部结果信号(**用户给的**,不是模型自评):用户纠正与重复请求是「上一轮判错 /
|
|
75
|
+
// 没做到」最直接的证据。带上当时生效的模式——落在哪个模式上,就是哪个模式在误判。
|
|
76
|
+
if (explicitCorrection || repeatedRequest)
|
|
77
|
+
deps.recordMetric({
|
|
78
|
+
kind: "outcome",
|
|
79
|
+
at: Date.now(),
|
|
80
|
+
sid,
|
|
81
|
+
turn: st.turnIndex,
|
|
82
|
+
event: explicitCorrection ? "user-correction" : "repeat-request",
|
|
83
|
+
mode: st.interactionMode,
|
|
84
|
+
detail: normalized.slice(0, 120),
|
|
85
|
+
});
|
|
86
|
+
st.recentUserQueries.push(normalized);
|
|
87
|
+
if (st.recentUserQueries.length > 5)
|
|
88
|
+
st.recentUserQueries.shift();
|
|
89
|
+
st.recentTurns.push(`用户: ${text.slice(0, 300)}`);
|
|
90
|
+
if (st.recentTurns.length > 12)
|
|
91
|
+
st.recentTurns.shift();
|
|
92
|
+
// 模式/阶段的冻结统一由 resolveIntent(组装时读会话权威历史)负责;这里只记账。
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case "assistant/message": {
|
|
96
|
+
const text = deps.visibleText(event.data?.message);
|
|
97
|
+
if (!text)
|
|
98
|
+
break;
|
|
99
|
+
st.assistantText = text;
|
|
100
|
+
collectAssistantFacts(sid, st, deps, text, event.data);
|
|
101
|
+
checkRequirementDrift(sid, st, deps, text);
|
|
102
|
+
checkContextPressure(sid, st, deps, event.data);
|
|
103
|
+
checkEvidenceNotices(st, deps, text);
|
|
104
|
+
st.recentTurns.push(`助手: ${text.slice(0, 300)}`);
|
|
105
|
+
if (st.recentTurns.length > 12)
|
|
106
|
+
st.recentTurns.shift();
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case "tool/call": {
|
|
110
|
+
st.toolCalls++;
|
|
111
|
+
if (st.interactionMode === "execute")
|
|
112
|
+
st.taskPhase = deps.advancePhase(st.taskPhase, "execute");
|
|
113
|
+
// 行为类别在调用阶段记账(连击),成败到结果阶段才结算。
|
|
114
|
+
st.toolKind = deps.classifyTool(deps.toolNameOf(event.data));
|
|
115
|
+
deps.applyToolSignal(st.triggerCounters, st.toolKind, null);
|
|
116
|
+
// 自动改动台账:mutate 类工具一被调用就先记一条——实测模型几乎不会主动调 lume_change
|
|
117
|
+
// (4 个会话里 0 次),而 edit/write 每次会话几十次。载具必须由插件自己落账,
|
|
118
|
+
// 否则「改动台账」永远空着(这正是上一版没生效的地方)。
|
|
119
|
+
if (st.toolKind === "inspect" && deps.toolTargetOf(event.data))
|
|
120
|
+
st.triggerCounters.codeInspects++;
|
|
121
|
+
// 越权改动:判成问答却动了文件——「路由判错」的机械证据,比用户抱怨出现得更早。
|
|
122
|
+
// 决策分档的机械判据:**已存在的文件**在本会话没看过就改 = 拿推测当依据。
|
|
123
|
+
// - 只管已存在的目标:新建文件天然「没读过」,对它喊「先读一次」是错话(外部审核指出);
|
|
124
|
+
// - 看过的判据只走 inspectedTarget(证据索引 + 摸过的目标,同一套 pathKey),
|
|
125
|
+
// 不再让两处各问一次、各用一种键。
|
|
126
|
+
if (st.toolKind === "mutate") {
|
|
127
|
+
const target = existingTarget(st, deps.toolTargetOf(event.data));
|
|
128
|
+
if (target && !inspectedTarget(st, target)) {
|
|
129
|
+
st.triggerCounters.unfoundedChanges++;
|
|
130
|
+
st.agent.lastBlindTarget = target;
|
|
131
|
+
}
|
|
132
|
+
else
|
|
133
|
+
st.agent.lastBlindTarget = null;
|
|
134
|
+
}
|
|
135
|
+
if (st.toolKind === "mutate" && st.interactionMode === "question")
|
|
136
|
+
deps.recordMetric({
|
|
137
|
+
kind: "outcome",
|
|
138
|
+
at: Date.now(),
|
|
139
|
+
sid,
|
|
140
|
+
turn: st.turnIndex,
|
|
141
|
+
event: "overreach",
|
|
142
|
+
mode: "question",
|
|
143
|
+
detail: deps.toolNameOf(event.data),
|
|
144
|
+
});
|
|
145
|
+
// 引用-证据对齐与定位门槛的输入:read 的窗口、摸过的目标、最近一次调用的命令文本。
|
|
146
|
+
// 只把 read/view 这类**读文件**的工具记成窗口——grep 的 path 可能只是目录或模式,
|
|
147
|
+
// 记成"整文件读过"会把没看的行洗白(宁可少记,也不要给假证据)。
|
|
148
|
+
const callName = deps.toolNameOf(event.data);
|
|
149
|
+
const callArgs = deps.toolArgsOf(event.data);
|
|
150
|
+
st.agent.lastToolName = callName || null;
|
|
151
|
+
st.agent.lastToolArgs = callArgs ? JSON.stringify(callArgs) : null;
|
|
152
|
+
st.agent.lastToolTarget = deps.toolTargetOf(event.data);
|
|
153
|
+
if (st.toolKind === "inspect") {
|
|
154
|
+
if (/read|view|cat|open|head|tail/i.test(callName))
|
|
155
|
+
deps.recordReadArgs(st.agent.evidence, callArgs);
|
|
156
|
+
if (st.agent.lastToolTarget)
|
|
157
|
+
st.agent.inspectedTargets.add(st.agent.lastToolTarget);
|
|
158
|
+
}
|
|
159
|
+
if (deps.projectMemoryOn && st.toolKind === "mutate") {
|
|
160
|
+
const target = deps.toolTargetOf(event.data);
|
|
161
|
+
if (target) {
|
|
162
|
+
const toolName = deps.toolNameOf(event.data);
|
|
163
|
+
// 台账条目要能当交付依据用,所以带上内容摘要(原来只有「由 edit 修改」,对模型零信息)
|
|
164
|
+
const args = deps.toolArgsOf(event.data);
|
|
165
|
+
const summary = deps.summarizeToolChange(args, toolName);
|
|
166
|
+
// 文档类产物留正文:覆盖核对要把「需求原句」与「交付物里的句子」并列(这是查矛盾的机械手段)
|
|
167
|
+
if (deps.DOC_ARTIFACT_RE.test(target)) {
|
|
168
|
+
const content = deps.toolArtifactText(args);
|
|
169
|
+
if (content) {
|
|
170
|
+
st.agent.artifactText = (st.agent.artifactText + "\n" + content).slice(-60000);
|
|
171
|
+
deps.clearNotice(st, "coverage"); // 换了新产物 → 重新核一次
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
deps.projectTask(sid, "自动改动入账", (store) => store.upsertChange(sid, {
|
|
175
|
+
target,
|
|
176
|
+
change: `(自动)${toolName}:${summary}`,
|
|
177
|
+
why: "",
|
|
178
|
+
verify: "",
|
|
179
|
+
status: "done",
|
|
180
|
+
at: Date.now(),
|
|
181
|
+
}));
|
|
182
|
+
}
|
|
183
|
+
// 首改前的定位门槛:要改的文件本会话从没被读过就动手 → 顶一次(不改代码,只补定位)
|
|
184
|
+
if (st.triggerCounters.mutations === 1 && target && !inspectedTarget(st, target)) {
|
|
185
|
+
const seen = [...st.agent.inspectedTargets].slice(-3).join("、") || "(本会话还没读过任何文件)";
|
|
186
|
+
if (!deps.noticeText(st, "trigger"))
|
|
187
|
+
deps.forceNotice(st, "trigger", `〔先定位〕你要改 ${target},但本会话还没有读过它——已经摸过的是:${seen}。先打开要改的那段(含调用方与配置/SQL 绑定),确认现有实现再动手;改完立刻回读或跑最小验证。`);
|
|
188
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 首改未定位:${target}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
case "tool/result": {
|
|
194
|
+
const data = event.data;
|
|
195
|
+
const resultText = deps.messageText(data?.message);
|
|
196
|
+
const explicitError = Boolean(data?.error) || looksLikeFailure(resultText);
|
|
197
|
+
const unknownResult = /结果未知|outcome unknown|tool_not_started|tool_outcome_unknown/i.test(resultText);
|
|
198
|
+
if (unknownResult)
|
|
199
|
+
st.toolUnknown++;
|
|
200
|
+
else if (explicitError)
|
|
201
|
+
st.toolFailures++;
|
|
202
|
+
else
|
|
203
|
+
st.toolSuccesses++;
|
|
204
|
+
if (st.interactionMode === "execute")
|
|
205
|
+
st.taskPhase = deps.advancePhase(st.taskPhase, unknownResult || explicitError ? "diagnose" : "verify");
|
|
206
|
+
// 行为信号 → 计数器 → 触发器提醒。提醒只在这一步之后可见(尾部快照),
|
|
207
|
+
// 且每类触发器有轮级冷却:提示一多就变噪音,模型会学会忽略。
|
|
208
|
+
const signals = deps.readResultSignals(resultText, explicitError);
|
|
209
|
+
deps.applyVerifyOutcome(st.triggerCounters, st.toolKind, signals);
|
|
210
|
+
// 引用-证据对齐:结果里出现过的「路径:行」也算"看到过"(grep 命中即证据)
|
|
211
|
+
if (st.toolKind === "inspect" && resultText)
|
|
212
|
+
deps.recordResultText(st.agent.evidence, resultText);
|
|
213
|
+
// 所有工具结果都算「见过」:否定断言只能用见过的东西支撑
|
|
214
|
+
if (resultText)
|
|
215
|
+
deps.recordSymbols(st.agent.seenSymbols, resultText);
|
|
216
|
+
// 验证结算:成功的真验证自动推进台账 / 失败立刻顶一句先修红——都不等模型调工具
|
|
217
|
+
if (deps.projectMemoryOn)
|
|
218
|
+
deps.settleVerification(sid, st, resultText, signals);
|
|
219
|
+
// 度量:出现一次「真验证命令」就记一条。它是 verify 类触发器效能判定的判据——
|
|
220
|
+
// **不能**用「台账 verified 增加」:那是 settleVerification 自动推进的产物,会自我表扬。
|
|
221
|
+
if (st.toolKind === "verify" && deps.isRealVerifyCommand(st.agent.lastToolArgs ?? ""))
|
|
222
|
+
deps.recordMetric({
|
|
223
|
+
kind: "outcome",
|
|
224
|
+
at: Date.now(),
|
|
225
|
+
sid,
|
|
226
|
+
turn: st.turnIndex,
|
|
227
|
+
event: "verify-run",
|
|
228
|
+
mode: st.interactionMode,
|
|
229
|
+
});
|
|
230
|
+
// 自动沉淀项目知识:**不依赖模型自觉调工具**(实测 3 次 lume_project_note 全丢)。判据在
|
|
231
|
+
// core/knowledge.ts(宁窄勿宽 + 敏感词硬拦),每会话有上限,落盘统一走 pendingFacts → flush。
|
|
232
|
+
if (deps.projectMemoryOn && resultText && st.agent.autoFacts < AUTO_FACT_CAP) {
|
|
233
|
+
for (const candidate of deps.extractKnowledgeCandidates(resultText, { source: "tool", userText: st.userText ?? "" })) {
|
|
234
|
+
if (st.agent.autoFacts >= AUTO_FACT_CAP)
|
|
235
|
+
break;
|
|
236
|
+
const fact = deps.normalizeProjectFact({ kind: candidate.kind, text: candidate.text }, Date.now(), {
|
|
237
|
+
taskTitle: st.sessionTitle,
|
|
238
|
+
});
|
|
239
|
+
if (!fact || deps.looksSensitive(fact.text))
|
|
240
|
+
continue;
|
|
241
|
+
st.pendingFacts.push(fact);
|
|
242
|
+
if (st.pendingFacts.length > 8)
|
|
243
|
+
st.pendingFacts.shift();
|
|
244
|
+
st.agent.autoFacts++;
|
|
245
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 自动沉淀候选(${fact.kind}):${fact.text.slice(0, 60)}`);
|
|
246
|
+
}
|
|
247
|
+
deps.flushPendingFacts(sid, event.data);
|
|
248
|
+
}
|
|
249
|
+
if (deps.behaviorTriggersOn) {
|
|
250
|
+
const fire = deps.evaluateToolTrigger(st.triggerCounters, {
|
|
251
|
+
turnIndex: st.turnIndex,
|
|
252
|
+
isTask: deps.isTaskQuery(st),
|
|
253
|
+
diagnosing: st.interactionMode === "diagnosis",
|
|
254
|
+
hasContract: deps.contractOf(sid) !== null,
|
|
255
|
+
unverifiedChanges: deps.changesOf(sid).filter((item) => item.status !== "verified" && item.status !== "skipped").length,
|
|
256
|
+
hasDesign: deps.designOf(sid).length > 0,
|
|
257
|
+
designSignal: deps.DESIGN_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? ""),
|
|
258
|
+
hypothesesTouched: st.hypothesesTouched,
|
|
259
|
+
blindTarget: st.agent.lastBlindTarget ?? null,
|
|
260
|
+
}, deps.triggerThresholds);
|
|
261
|
+
if (fire && deps.cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
262
|
+
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
263
|
+
// 度量:记下命中**当时的计数器快照**,之后「行为是否真的变了」才有基线可比。
|
|
264
|
+
deps.recordMetric({
|
|
265
|
+
kind: "trigger",
|
|
266
|
+
at: Date.now(),
|
|
267
|
+
sid,
|
|
268
|
+
turn: st.turnIndex,
|
|
269
|
+
id: fire.id,
|
|
270
|
+
counters: { ...st.triggerCounters },
|
|
271
|
+
});
|
|
272
|
+
deps.forceNotice(st, "trigger", fire.text);
|
|
273
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 行为触发器 ${fire.id}(steps=${st.triggerCounters.steps},inspect=${st.triggerCounters.inspectStreak},mutate=${st.triggerCounters.mutateStreak},verifyFail=${st.triggerCounters.verifyFailStreak})`);
|
|
274
|
+
// 死路落成项目知识:句柄取一次再收窄(可空),后台写失败留痕
|
|
275
|
+
const store = deps.projectOf();
|
|
276
|
+
if (fire.id === "dead-path" && signals.env && store) {
|
|
277
|
+
const key = deps.projectKeyFor(sid, session);
|
|
278
|
+
if (key) {
|
|
279
|
+
const fact = deps.normalizeProjectFact({
|
|
280
|
+
kind: "deadend",
|
|
281
|
+
text: `本环境验证受阻(${st.triggerCounters.verifyFailStreak} 次连续失败,环境/依赖类):换降级阶梯,不要重复同一命令`,
|
|
282
|
+
}, Date.now());
|
|
283
|
+
if (fact) {
|
|
284
|
+
void store
|
|
285
|
+
.addFact(key, fact, (candidate, existing) => existing.some((f) => f.text === candidate))
|
|
286
|
+
.catch((error) => deps.ctx.logger?.warn?.(`lume: [${sid}] 死路知识落盘失败`, error));
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "compaction/summary": {
|
|
295
|
+
// 压缩由宿主 preset 在隔离域执行(Lume 无法接管该服务),但事件在
|
|
296
|
+
// 会话总线上可见。记录规模,供下一轮注入「摘要不是完整历史」的重锚。
|
|
297
|
+
const data = event.data;
|
|
298
|
+
const shadowedItems = Array.isArray(data?.shadowedSeqs) ? data.shadowedSeqs.length : 0;
|
|
299
|
+
const tokens = typeof data?.shadowedTokenCount === "number" ? data.shadowedTokenCount : 0;
|
|
300
|
+
st.compaction = { turnIndex: st.turnIndex, shadowedItems, tokens };
|
|
301
|
+
deps.appendLumeLog(`[${sid}] 压缩完成:替换 ${shadowedItems} 项历史(~${tokens} tokens),下一轮注入状态重锚`);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case "turn/end": {
|
|
305
|
+
// 每轮把会话记忆搬出来(零 token、机械):上下文不能当记忆载体——
|
|
306
|
+
// 它撑满时宿主压缩会失败(现场:context overflow),之后会话再也产不出事件。
|
|
307
|
+
void deps.saveSessionMemory(sid);
|
|
308
|
+
handleTurnEnd(deps, sid, st, session);
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
default:
|
|
312
|
+
// 诊断:压缩事件是否经由 session/event 总线投递(宿主按 session
|
|
313
|
+
// 所属上下文收集监听者,隔离域里发出的日志事件可能不经过这里)。
|
|
314
|
+
if (typeof event.type === "string" && /compact/i.test(event.type)) {
|
|
315
|
+
deps.appendLumeLog(`[${sid}] 收到未处理的压缩事件类型 ${event.type}`);
|
|
316
|
+
}
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 会话结束:先最后试一次项目知识补落盘(此时 session 自带 cwd),再归档反思、清运行时。
|
|
323
|
+
* 与事件处理器共用同一批依赖(sessionEventDeps)。
|
|
324
|
+
*/
|
|
325
|
+
export function createSessionDisposedHandler(deps) {
|
|
326
|
+
return (session) => {
|
|
327
|
+
const sid = String(session.id);
|
|
328
|
+
const st = deps.runtime.get(sid);
|
|
329
|
+
const turns = [...st.recentTurns];
|
|
330
|
+
// 会话结束前最后试一次补落盘(session 自带 cwd);仍然落不了就如实报数,不再静默丢弃。
|
|
331
|
+
deps.flushPendingFacts(sid, session);
|
|
332
|
+
if (st.pendingFacts.length > 0)
|
|
333
|
+
deps.ctx.logger?.warn?.(`lume: [${sid}] 项目知识未落盘(无法确定工作目录):${st.pendingFacts.length} 条`);
|
|
334
|
+
deps.runtime.delete(sid);
|
|
335
|
+
// 任务载具是会话态:任务结束即无意义,清掉避免无界增长(项目知识在另一张表,不受影响)。
|
|
336
|
+
deps.projectTask(sid, "清空会话台账", (store) => store.clearSession(sid));
|
|
337
|
+
// 反思日志:会话结束后空闲时间跑一次小模型,零用户感知 token。
|
|
338
|
+
// 历史不够长(< 4 条消息)或路由不可用时静默跳过。
|
|
339
|
+
if (deps.reflectionEnabled && turns.length < 4)
|
|
340
|
+
deps.ctx.logger?.warn?.(`lume: 反思跳过(${sid})历史不足:${turns.length} < 4 条消息`);
|
|
341
|
+
if (deps.reflectionEnabled && turns.length >= 4) {
|
|
342
|
+
void (async () => {
|
|
343
|
+
const store = await deps.reflectionReady;
|
|
344
|
+
if (!store) {
|
|
345
|
+
deps.ctx.logger?.warn?.(`lume: 反思跳过(${sid})reflection 域不可用`);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (!store)
|
|
349
|
+
return;
|
|
350
|
+
const route = deps.resolveAuxRoute({}, deps.llmRoute.current);
|
|
351
|
+
if (!route) {
|
|
352
|
+
deps.ctx.logger?.warn?.(`lume: 反思跳过(${sid})无可用小模型路由`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (!route)
|
|
356
|
+
return;
|
|
357
|
+
const prompt = deps.buildReflectionPrompt(turns);
|
|
358
|
+
const output = await deps.callLlm(route, prompt.system, prompt.userText, 800);
|
|
359
|
+
if (output === null)
|
|
360
|
+
return;
|
|
361
|
+
const score = deps.parseReflectionScore(output);
|
|
362
|
+
if (!score) {
|
|
363
|
+
deps.ctx.logger?.warn?.(`lume: 反思跳过(${sid})评分解析失败`);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await store.log(sid, score);
|
|
367
|
+
deps.ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review} diagnosis=${score.diagnosis}「${score.note}」`);
|
|
368
|
+
})();
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -17,24 +17,37 @@ function defaultRuntime() {
|
|
|
17
17
|
lastExtractionAt: undefined,
|
|
18
18
|
lastExchange: null,
|
|
19
19
|
recentTurns: [],
|
|
20
|
-
|
|
20
|
+
sessionTitle: "",
|
|
21
|
+
taskMemories: null,
|
|
22
|
+
contextWindow: 0,
|
|
21
23
|
lastFailureQuery: null,
|
|
22
24
|
failureStreak: 0,
|
|
23
25
|
interactionMode: "question",
|
|
24
26
|
intent: null,
|
|
25
27
|
stableDigest: null,
|
|
28
|
+
cwd: null,
|
|
29
|
+
requirementFresh: false,
|
|
30
|
+
driftWordsReported: [],
|
|
31
|
+
notices: {},
|
|
32
|
+
agent: {
|
|
33
|
+
evidence: new Map(),
|
|
34
|
+
artifactText: "",
|
|
35
|
+
inspectedTargets: new Set(),
|
|
36
|
+
seenSymbols: new Set(),
|
|
37
|
+
lastToolName: null,
|
|
38
|
+
lastToolArgs: null,
|
|
39
|
+
lastToolTarget: null,
|
|
40
|
+
autoFacts: 0,
|
|
41
|
+
},
|
|
42
|
+
pendingFacts: [],
|
|
26
43
|
projectKey: null,
|
|
27
44
|
toolKind: "other",
|
|
28
45
|
triggerCounters: newTriggerCounters(),
|
|
29
|
-
triggerNudge: null,
|
|
30
46
|
triggerFiredAt: {},
|
|
31
|
-
turnNudge: null,
|
|
32
47
|
lastDriftTurn: null,
|
|
33
48
|
knowledgePrompted: false,
|
|
34
49
|
hypothesesTouched: false,
|
|
35
|
-
alignmentCorrection: null,
|
|
36
50
|
recentUserQueries: [],
|
|
37
|
-
postTurnReview: null,
|
|
38
51
|
taskPhase: "answer",
|
|
39
52
|
toolCalls: 0,
|
|
40
53
|
toolSuccesses: 0,
|
package/lib/host/thinking.js
CHANGED
|
@@ -35,6 +35,14 @@ export const THINKING_TEXT = `[任务执行协议]
|
|
|
35
35
|
|
|
36
36
|
**P2 证据时效**:日志、历史记录、报错文本、旧结论都带时间。引用它们作为证据前先核对时间戳是否落在当前问题的时间窗口内,并确认因果关系——**历史里存在的错误不等于当前问题的原因**。日志里翻到一条报错就直接当成用户当前症状的解释,是最常见的误判。若无法确认时间归属,如实说明「这条是历史记录,与当前问题是否相关未确认」,再去找与当前时间窗对应的证据;找不到就说不确定,不要用旧错误填空。
|
|
37
37
|
|
|
38
|
+
**P1 独立判断**:用户给的是需求,不是技术结论。用户说的做法要先独立评估:同意要说清为什么成立,不同意要给具体失败场景和更安全的替代。允许「有条件同意」(前提对、但要加限定),不要把用户的判断直接当成自己的结论,也不要用复述他的话代替评估。
|
|
39
|
+
|
|
40
|
+
**P1 证据来源纪律**:判断「现状」以**当前代码和当前运行结果**为准;git 历史、旧日志只用于回答"何时引入/当时有没有配套脚本"这类时间问题,且要在回答里写明为什么用历史。你写下的每一处「文件:行」都应该是你**这次真的打开过**的那一段——同一符号在不同路径(新增/导入/导出/定时任务)里的处理常常不一样,不要用一条路径的代码去断定另一条路径的行为。
|
|
41
|
+
|
|
42
|
+
**P0 事实优先**:答案依赖仓库事实(提交历史、文件内容、接口定义、表结构)时,**先用只读工具核实再回答**。禁止用「我不确定」「我这轮没接上上下文」「这是新会话」当答案——那不是回答,是把核实工作退回给用户。确实查不到(要账号 / 要看生产库 / 需要用户提供)才说查不到,并写明查到哪一步。
|
|
43
|
+
|
|
44
|
+
**P1 提问纪律**:把决定权交回用户之前,先核实这条问题的**前提**——一次 grep/read 就能确认的,先自己确认,不要拿它当问题问用户;能按现有证据定下的,直接定下并写明默认假设("我按 X 做,除非你反对"),不要挂在"待你定"里。每轮抛回给用户的"待确认"不超过 2 条(默认 0 条),且**不许把"我没查环境/我没登录/我猜"的推断列成待你定**。**已核实的事实不要再挂"待确认"**:你读过的代码已经能定的就定下来(写明依据)——把能定的事标成待确认,等于把工作退回给用户。用户问"方案清楚了吗"时要给清单,但清单里的每一项要么已定(写明依据),要么是真阻塞(写清你查到哪一步)。
|
|
45
|
+
|
|
38
46
|
**P2 达成标准**:完成动作不等于达成目标。交付前必须回答:用户要的结果是否已经出现?用户能否实际使用?是否引入了需要用户清理的中间文件、配置、会话或其他副作用?
|
|
39
47
|
|
|
40
48
|
**P2 振荡预防**:同一假设连续失败后停止重复尝试,记录失败原因并换方案。已排除的假设不再重提;不使用破坏性命令绕过问题;不把测试删掉或放宽断言来制造假成功。
|
|
@@ -59,7 +67,8 @@ export const TASK_SIGNAL_RE = /代码|编程|文件|项目|仓库|脚本|命令|
|
|
|
59
67
|
export const REASONING_MODEL_RE = /deepseek-v[345]|reason|o[134]|gpt-5/i;
|
|
60
68
|
/** 推理型模型的任务协议:省掉它天生具备的计划/分解条款,保留行为约束与事实边界。 */
|
|
61
69
|
export const THINKING_REASONING_TEXT = `[任务执行协议]
|
|
62
|
-
|
|
70
|
+
已确认当前模型具备推理能力。仍须保护用户改动,修改后立即验证;失败先归因并更换方案,不重复已排除假设;完成前复核需求、边界和数据保留。引用日志、历史记录或旧报错作为证据时先核对时间戳与因果:历史错误不等于当前问题的原因,确认不了就明说。示例、历史消息和角色记忆只作表达与相关性参考,不自动等于当前事实。人设只影响表达,不影响代码、工具调用和安全判断。
|
|
71
|
+
独立判断:用户给的是需求,不是技术结论——同意要说清为什么成立,不同意要给具体失败场景,允许"有条件同意"。证据来源纪律:判断现状以当前代码为准,历史只用于时间问题;写下的「文件:行」必须是你这次真的打开过的那段,同一符号在不同路径(新增/导入/导出/定时任务)的处理常常不同,不要用一条路径的代码断定另一条路径的行为。提问纪律:把决定权交回用户前先核实问题前提(一次 grep/read 能确认的先自己确认),能定的就定下并写明默认假设,"待确认"不超过 2 条,不许把"我没查环境/我猜"的推断列成待你定。`;
|
|
63
72
|
/**
|
|
64
73
|
* 按「是否任务型 × 是否推理模型」选择协议变体。
|
|
65
74
|
* 路由未知(模型名缺失)时回退完整版,避免误判造成能力退化。
|