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
package/lib/index.js
CHANGED
|
@@ -25,26 +25,50 @@ import { makeRpcRoute } from "./host/rpc-bridge.js";
|
|
|
25
25
|
import { FilePersonaStore, migrateLegacyState, PersonaStore } from "./host/store.js";
|
|
26
26
|
import { IdentityStore, LUME_IDENTITY_SPEC, zodLike } from "./host/identity.js";
|
|
27
27
|
import { PersonaRegistry } from "./host/registry.js";
|
|
28
|
-
import { buildCorrectionPrompt, buildExtractionPrompt, extractNaming, isCoolingDown, isDuplicateFact, mergeNewFacts, parseCorrectionRule, parseFacts, resolveAuxRoute, shouldCaptureCorpus, shouldConsider, shouldConsiderCorrection } from "./host/extraction.js";
|
|
28
|
+
import { buildCorrectionPrompt, buildExtractionPrompt, extractNaming, isCoolingDown, isDuplicateFact, mergeNewFacts, parseCorrectionRule, parseFacts, resolveAuxRoute, shouldCaptureCorpus, shouldConsider, shouldConsiderCorrection, } from "./host/extraction.js";
|
|
29
29
|
import { DistillJobRunner, DISTILL_ALGORITHM_VERSION, runDistill } from "./host/distill.js";
|
|
30
30
|
import { jaccard } from "./core/retrieval.js";
|
|
31
31
|
import { fnv1a32 } from "./core/sampling.js";
|
|
32
32
|
import { detectLeak } from "./core/leak-detector.js";
|
|
33
|
-
import { messageText } from "./core/text.js";
|
|
33
|
+
import { messageText, visibleText } from "./core/text.js";
|
|
34
|
+
import { formatWindows, recordReadArgs, recordResultText, unsupportedCitations } from "./core/citations.js";
|
|
34
35
|
import { composeBoundary } from "./host/boundary.js";
|
|
35
36
|
import { SessionRuntimeStore } from "./host/session-runtime.js";
|
|
36
37
|
import { isCompactionCheckpoint } from "./host/compaction.js";
|
|
37
38
|
import { LUME_REFLECTION_SPEC, ReflectionStore, buildReflectionPrompt, parseReflectionScore } from "./host/reflection.js";
|
|
38
39
|
import { appendLumeLog } from "./host/diag.js";
|
|
39
|
-
import {
|
|
40
|
+
import { clearNotice, forceNotice, noticeOpen, noticeText, setNotice } from "./host/notices.js";
|
|
41
|
+
import { toolArgsOf, toolNameOf, toolTargetOf } from "./host/host-events.js";
|
|
42
|
+
import { advancePhase, buildAlignmentCorrection, buildCasualDirective, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyWithTrajectory, isUserAuthored, taskPhaseForMode, } from "./host/protocol.js";
|
|
43
|
+
import { DESIGN_SIGNAL_RE } from "./host/protocol.js";
|
|
40
44
|
import { buildDocumentDirective, probeDocumentCapabilities } from "./host/documents.js";
|
|
41
45
|
import { REASONING_MODEL_RE, TASK_SIGNAL_RE, selectStableThinkingProtocol } from "./host/thinking.js";
|
|
42
|
-
import { normalizeChange, normalizeContract, normalizeHypothesis, normalizeProjectFact, projectKeyOf, renderChangeLedger, renderContract, renderHypotheses, renderProjectFacts } from "./core/ledger.js";
|
|
46
|
+
import { normalizeChange, normalizeContract, normalizeHypothesis, normalizeProjectFact, projectKeyOf, renderChangeLedger, renderContract, renderHypotheses, renderProjectFacts, } from "./core/ledger.js";
|
|
47
|
+
import { normalizeDesign, renderDesign, renderRequirements } from "./core/ledger.js";
|
|
43
48
|
import { classifyTool, readResultSignals } from "./core/signals.js";
|
|
44
|
-
import {
|
|
49
|
+
import { auditOpenQuestions, isRealVerifyCommand, summarizeToolChange, toolArtifactText, unrequestedChangeWords, } from "./core/signals.js";
|
|
50
|
+
import { recordSymbols, unsupportedClaims } from "./core/citations.js";
|
|
51
|
+
import { coverageRows, danglingSectionRefs, hasFigureRefs, pickRequirementCorpus, splitRequirementItems } from "./core/coverage.js";
|
|
52
|
+
/** 〔提问核对〕每会话上限(提问纪律的纠偏;比引用核对更敏感,限得更死)。 */
|
|
53
|
+
const QUESTION_AUDIT_MAX = 2;
|
|
54
|
+
/** 只把「文档类产物」当交付物收进覆盖核对(源码改动进去只会制造噪音)。 */
|
|
55
|
+
const DOC_ARTIFACT_RE = /\.(md|markdown|txt)$/i;
|
|
56
|
+
import { buildCarrierGapNotice, buildCitationDirective, buildClaimDirective, buildContractMethodDirective, buildDocumentMethodDirective, buildImpactDirective, buildQuestionAuditDirective, buildRequirementCoverageDirective, buildStructureHint, buildUnverifiedDeliveryNotice, composeBlocksDetailed, } from "./host/methods.js";
|
|
57
|
+
import { buildDesignMethodDirective, buildRequirementMethodDirective, buildDriftDirective } from "./host/methods.js";
|
|
45
58
|
import { LUME_PROJECT_SPEC, ProjectStore } from "./host/project.js";
|
|
46
|
-
import {
|
|
47
|
-
|
|
59
|
+
import { volatileBlocks } from "./host/prompt-blocks.js";
|
|
60
|
+
import { initStores } from "./host/bootstrap.js";
|
|
61
|
+
import { focusIdsFor } from "./host/clauses.js";
|
|
62
|
+
import { createMetricsLog } from "./host/metrics-log.js";
|
|
63
|
+
import { createAuxLlm } from "./host/llm-aux.js";
|
|
64
|
+
import { createLlmRouteCell } from "./host/llm-route.js";
|
|
65
|
+
import { createProjectAccess } from "./host/project-access.js";
|
|
66
|
+
import { installPromptSections } from "./host/sections.js";
|
|
67
|
+
import { registerLumeTools } from "./host/tools.js";
|
|
68
|
+
import { assembleBlockDeps, assembleSessionEventDeps, assembleToolDeps, startSessionBackfill } from "./host/wiring.js";
|
|
69
|
+
import { createSessionDisposedHandler, createSessionEventHandler } from "./host/session-events.js";
|
|
70
|
+
import { DEFAULT_TRIGGER_THRESHOLDS, applyToolSignal, applyVerifyOutcome, cooldownOk, evaluateToolTrigger, evaluateTurnTrigger, } from "./host/triggers.js";
|
|
71
|
+
/** schemastery → domainTable 形参的桥接(与 stores.identity().ts 同款)。 */
|
|
48
72
|
const recordSchema = zodLike;
|
|
49
73
|
/** 会话人设选择的持久层(键 = sessionId)。 */
|
|
50
74
|
export const LUME_DOMAIN_SPEC = defineDomain({
|
|
@@ -146,105 +170,46 @@ function applyInner(ctx, config = {}) {
|
|
|
146
170
|
}
|
|
147
171
|
const defaultName = builtins[NONE_PERSONA] ? NONE_PERSONA : null;
|
|
148
172
|
const legacyStatePath = join(assetsDir, "persona-state.json");
|
|
149
|
-
// ──
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const migrated = await migrateLegacyState(store, legacyStatePath);
|
|
160
|
-
if (migrated)
|
|
161
|
-
ctx.logger?.warn?.("lume: 已从 assets/persona-state.json 迁移旧的人设记忆");
|
|
162
|
-
return store;
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
ctx.logger?.warn?.("lume: storageDomain 不可用,降级为 assets 文件存储", error);
|
|
166
|
-
return new FilePersonaStore(legacyStatePath, { maxSessions: MAX_SESSIONS });
|
|
167
|
-
}
|
|
168
|
-
})();
|
|
169
|
-
const identityReady = (async () => {
|
|
170
|
-
try {
|
|
171
|
-
const domain = await ctx.storageDomain.open(LUME_IDENTITY_SPEC);
|
|
172
|
-
ctx.effect(() => async () => {
|
|
173
|
-
await domain.close();
|
|
174
|
-
}, "lume: close identity domain");
|
|
175
|
-
return new IdentityStore({
|
|
176
|
-
profile: domain.table("profile"),
|
|
177
|
-
memory_facts: domain.table("memory_facts"),
|
|
178
|
-
style_rules: domain.table("style_rules"),
|
|
179
|
-
corpus_pins: domain.table("corpus_pins"),
|
|
180
|
-
custom_personas: domain.table("custom_personas"),
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
catch (error) {
|
|
184
|
-
ctx.logger?.warn?.("lume: 身份域不可用,档案/记忆/自定义人设功能降级", error);
|
|
185
|
-
return null;
|
|
186
|
-
}
|
|
187
|
-
})();
|
|
188
|
-
void storesReady.then((store) => {
|
|
189
|
-
currentStore = store;
|
|
190
|
-
});
|
|
191
|
-
void identityReady.then((store) => {
|
|
192
|
-
identity = store;
|
|
173
|
+
// ── 存储与生命周期(会话/身份/反思/项目四域)── 实现见 host/bootstrap.ts
|
|
174
|
+
const stores = initStores({
|
|
175
|
+
ctx,
|
|
176
|
+
legacyStatePath,
|
|
177
|
+
maxSessions: MAX_SESSIONS,
|
|
178
|
+
projectMemoryOn: config.projectMemory ?? true,
|
|
179
|
+
migrateLegacyState,
|
|
180
|
+
personaDomainSpec: LUME_DOMAIN_SPEC,
|
|
181
|
+
sessionPersonaTable: SESSION_PERSONA_TABLE,
|
|
182
|
+
describeError,
|
|
193
183
|
});
|
|
194
|
-
// ── 反思域(会话结束后打分,失败降级为无反思功能)──
|
|
195
|
-
let reflectionStore = null;
|
|
196
|
-
const reflectionReady = (async () => {
|
|
197
|
-
try {
|
|
198
|
-
const domain = await ctx.storageDomain.open(LUME_REFLECTION_SPEC);
|
|
199
|
-
ctx.effect(() => async () => { await domain.close(); }, "lume: close reflection domain");
|
|
200
|
-
const store = new ReflectionStore(domain.table("logs"));
|
|
201
|
-
const migrated = await store.migrateLegacy();
|
|
202
|
-
if (migrated > 0)
|
|
203
|
-
ctx.logger?.warn?.(`lume: 已迁移 ${migrated} 条旧版反思日志`);
|
|
204
|
-
return store;
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
ctx.logger?.warn?.("lume: 反思域不可用,反思日志降级", error);
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
})();
|
|
211
|
-
void reflectionReady.then((s) => { reflectionStore = s; });
|
|
212
184
|
// ── 项目域:任务契约 / 改动台账 / 假设台账 / 项目知识(失败降级为无载具功能)──
|
|
213
185
|
const projectMemoryOn = config.projectMemory ?? true;
|
|
214
186
|
const behaviorTriggersOn = config.behaviorTriggers ?? true;
|
|
187
|
+
/** 运行时度量开关(默认开):关掉只是不记录,不改变任何行为。 */
|
|
188
|
+
const metricsEnabled = config.metrics !== false;
|
|
215
189
|
const triggerThresholds = {
|
|
216
190
|
...DEFAULT_TRIGGER_THRESHOLDS,
|
|
217
191
|
inspectStreak: config.triggerInspectStreak ?? DEFAULT_TRIGGER_THRESHOLDS.inspectStreak,
|
|
218
192
|
changeStreak: config.triggerChangeStreak ?? DEFAULT_TRIGGER_THRESHOLDS.changeStreak,
|
|
219
193
|
deadPathFails: config.triggerDeadPathFails ?? DEFAULT_TRIGGER_THRESHOLDS.deadPathFails,
|
|
220
194
|
};
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch (error) {
|
|
236
|
-
ctx.logger?.warn?.("lume: 项目域不可用,任务契约/台账/项目知识降级", error);
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
})();
|
|
240
|
-
void projectReady.then((s) => { project = s; });
|
|
241
|
-
const registry = new PersonaRegistry(builtins, () => identity);
|
|
242
|
-
ctx.logger?.warn?.(`lume: 已加载(builtins=${Object.keys(builtins).join(",") || "空!"},assets=${assetsDir})`);
|
|
195
|
+
/** 请求是不是任务型(按形态决定方法块;提示装配与事件处理共用)。 */
|
|
196
|
+
function isTaskQuery(st) {
|
|
197
|
+
return TASK_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? "");
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* fire-and-forget 的持久化:**失败必须留痕**。
|
|
201
|
+
*
|
|
202
|
+
* 现场教训(2026-09-23):一整批写入路径用 `void stores.projectReady.then(...)` 且没有 catch,
|
|
203
|
+
* 出问题时不报错也不入账(ledger 一直 undefined),六个功能静默失效很久才被发现。
|
|
204
|
+
*/
|
|
205
|
+
const registry = new PersonaRegistry(builtins, () => stores.identity());
|
|
206
|
+
ctx.logger?.warn?.(`lume: 已加载(builtins=${Object.keys(builtins).join(",") || "空!"},assets=${assetsDir},能力=载具+触发器+设计pass+需求锚点)`);
|
|
243
207
|
ctx.logger?.warn?.(`lume: llmRoute 初始化策略:agentDefaultModel → settings → 回退`);
|
|
244
208
|
// ── 每会话运行时状态(内存,重启即弃,LRU 上限兜底)──
|
|
245
209
|
const runtime = new SessionRuntimeStore();
|
|
246
210
|
// ── 模型路由缓存(request/context,会话过程中由 agent-loop 更新)──
|
|
247
|
-
|
|
211
|
+
// 共享可变单元(不是快照):会话事件在 request/context 里写它,读方都拿 .current
|
|
212
|
+
const llmRoute = createLlmRouteCell();
|
|
248
213
|
// ── 主动解析默认模型:会话开始前蒸馏/提取也要能用 ──
|
|
249
214
|
// request/context 事件只在对话路由变化时触发(delta event),静默状态下 llmRoute 恒为 null,
|
|
250
215
|
// 导致蒸馏一开即报「模型路由不可用」。这里初始化即解析默认模型,后续仍被 request/context 覆盖。
|
|
@@ -258,8 +223,8 @@ function applyInner(ctx, config = {}) {
|
|
|
258
223
|
if (agentDefaultModel) {
|
|
259
224
|
const selection = agentDefaultModel.currentSelection?.();
|
|
260
225
|
if (typeof selection?.provider === "string" && typeof selection?.model === "string") {
|
|
261
|
-
llmRoute = { provider: selection.provider, model: selection.model };
|
|
262
|
-
ctx.logger?.warn?.(`lume: llmRoute 从 agentDefaultModel 初始化 → ${llmRoute.provider}/${llmRoute.model}`);
|
|
226
|
+
llmRoute.current = { provider: selection.provider, model: selection.model };
|
|
227
|
+
ctx.logger?.warn?.(`lume: llmRoute 从 agentDefaultModel 初始化 → ${llmRoute.current.provider}/${llmRoute.current.model}`);
|
|
263
228
|
return;
|
|
264
229
|
}
|
|
265
230
|
}
|
|
@@ -273,8 +238,8 @@ function applyInner(ctx, config = {}) {
|
|
|
273
238
|
if (settings) {
|
|
274
239
|
const raw = settings.get("agent-default-model");
|
|
275
240
|
if (raw && typeof raw.provider === "string" && typeof raw.model === "string") {
|
|
276
|
-
llmRoute = { provider: raw.provider, model: raw.model };
|
|
277
|
-
ctx.logger?.warn?.(`lume: llmRoute 从 settings 初始化 → ${llmRoute.provider}/${llmRoute.model}`);
|
|
241
|
+
llmRoute.current = { provider: raw.provider, model: raw.model };
|
|
242
|
+
ctx.logger?.warn?.(`lume: llmRoute 从 settings 初始化 → ${llmRoute.current.provider}/${llmRoute.current.model}`);
|
|
278
243
|
return;
|
|
279
244
|
}
|
|
280
245
|
}
|
|
@@ -284,98 +249,21 @@ function applyInner(ctx, config = {}) {
|
|
|
284
249
|
}
|
|
285
250
|
ctx.logger?.warn?.("lume: llmRoute 初始化失败 — 蒸馏/提取在对话前不可用");
|
|
286
251
|
})();
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
source: { kind: "plugin", plugin: "lume" },
|
|
303
|
-
}),
|
|
304
|
-
];
|
|
305
|
-
const assembler = new BlockAssembler();
|
|
306
|
-
try {
|
|
307
|
-
for await (const chunk of llm.stream({ provider: route.provider, model: route.model, messages, system, maxTokens, reasoningEffort: ReasoningEffortId("low"), temperature: 0, ...(signal ? { signal } : {}) })) {
|
|
308
|
-
assembler.push(chunk);
|
|
309
|
-
}
|
|
310
|
-
// 错误经流内 finish chunk 传输(不 throw)——检查 finish.kind === "error"
|
|
311
|
-
if (assembler.finish.kind === "error") {
|
|
312
|
-
const code = assembler.finish.failure?.code;
|
|
313
|
-
if (code !== "UNSUPPORTED_REASONING_EFFORT")
|
|
314
|
-
throw new Error(String(assembler.finish.failure?.message ?? "unnamed stream error"));
|
|
315
|
-
// 不支持 effort:降级无 effort 重发
|
|
316
|
-
const assembler2 = new BlockAssembler();
|
|
317
|
-
for await (const chunk of llm.stream({ provider: route.provider, model: route.model, messages, system, maxTokens, temperature: 0, ...(signal ? { signal } : {}) })) {
|
|
318
|
-
assembler2.push(chunk);
|
|
319
|
-
}
|
|
320
|
-
if (assembler2.finish.kind === "error") {
|
|
321
|
-
throw new Error(String(assembler2.finish.failure?.message ?? "unnamed stream error"));
|
|
322
|
-
}
|
|
323
|
-
return assembler2
|
|
324
|
-
.blocks()
|
|
325
|
-
.map((block) => {
|
|
326
|
-
const text = block?.text;
|
|
327
|
-
return typeof text === "string" ? text : "";
|
|
328
|
-
})
|
|
329
|
-
.filter((text) => text.length > 0)
|
|
330
|
-
.join(" ")
|
|
331
|
-
.trim();
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
catch (error) {
|
|
335
|
-
// throw 形态的错误:非 UNSUPPORTED 直接抛;是则降级重试
|
|
336
|
-
if (error?.code !== "UNSUPPORTED_REASONING_EFFORT")
|
|
337
|
-
throw error;
|
|
338
|
-
const assembler2 = new BlockAssembler();
|
|
339
|
-
for await (const chunk of llm.stream({ provider: route.provider, model: route.model, messages, system, maxTokens, temperature: 0, ...(signal ? { signal } : {}) })) {
|
|
340
|
-
assembler2.push(chunk);
|
|
341
|
-
}
|
|
342
|
-
if (assembler2.finish.kind === "error") {
|
|
343
|
-
throw new Error(String(assembler2.finish.failure?.message ?? "unnamed stream error"));
|
|
344
|
-
}
|
|
345
|
-
return assembler2
|
|
346
|
-
.blocks()
|
|
347
|
-
.map((block) => {
|
|
348
|
-
const text = block?.text;
|
|
349
|
-
return typeof text === "string" ? text : "";
|
|
350
|
-
})
|
|
351
|
-
.filter((text) => text.length > 0)
|
|
352
|
-
.join(" ")
|
|
353
|
-
.trim();
|
|
354
|
-
}
|
|
355
|
-
const allBlocks = assembler
|
|
356
|
-
.blocks()
|
|
357
|
-
.map((block) => {
|
|
358
|
-
const text = block?.text;
|
|
359
|
-
return typeof text === "string" ? text : "";
|
|
360
|
-
})
|
|
361
|
-
.filter((text) => text.length > 0);
|
|
362
|
-
// 诊断探针:完整输出落盘(含 max-tokens 截断标记;追加,一次失败可看全程)
|
|
363
|
-
try {
|
|
364
|
-
const existing = readFileSync(LLM_DUMP_PATH, "utf8");
|
|
365
|
-
const dumps = existing ? JSON.parse(existing) : [];
|
|
366
|
-
dumps.push({ ts: Date.now(), route: `${route.provider}/${route.model}`, maxTokens, finish: assembler.finish, blocks: allBlocks.map((t) => t.slice(0, 6000)) });
|
|
367
|
-
writeFileSync(LLM_DUMP_PATH, JSON.stringify(dumps, null, 2), "utf8");
|
|
368
|
-
}
|
|
369
|
-
catch { /* 诊断失败不阻断 */ }
|
|
370
|
-
return allBlocks.join(" ").trim();
|
|
371
|
-
}
|
|
372
|
-
catch (error) {
|
|
373
|
-
if (signal?.aborted)
|
|
374
|
-
throw error; // 用户取消:向上抛,任务状态走 cancelled
|
|
375
|
-
ctx.logger?.warn?.("lume: 小模型调用失败,本轮跳过", error);
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
}
|
|
252
|
+
// ── 辅助模型调用(实现见 host/llm-aux.ts)──
|
|
253
|
+
const { callLlm } = createAuxLlm({ ctx, llmRoute: () => llmRoute.current, llmDumpPath: LLM_DUMP_PATH });
|
|
254
|
+
// ── 载具与项目知识的读取入口(实现见 host/project-access.ts)──
|
|
255
|
+
const projectAccess = createProjectAccess({
|
|
256
|
+
ctx,
|
|
257
|
+
config,
|
|
258
|
+
runtime,
|
|
259
|
+
stores,
|
|
260
|
+
projectTask: stores.projectTask,
|
|
261
|
+
normalizeProjectFact,
|
|
262
|
+
isRealVerifyCommand,
|
|
263
|
+
jaccard,
|
|
264
|
+
projectKeyOf,
|
|
265
|
+
});
|
|
266
|
+
const { projectKeyFor, commandSummary, flushPendingFacts, settleVerification, saveSessionMemory, taskMemoriesOf, contractOf, changesOf, hypothesesOf, factsOf, requirementsOf, designOf, needsDesignPass, structureToolName, } = projectAccess;
|
|
379
267
|
/** 被动提取:三道门 → 小模型 → 合并落盘。按会话串行,失败静默。 */
|
|
380
268
|
function scheduleExtraction(sid, st) {
|
|
381
269
|
if (st.extracting) {
|
|
@@ -396,7 +284,7 @@ function applyInner(ctx, config = {}) {
|
|
|
396
284
|
st.userText = "";
|
|
397
285
|
st.assistantText = "";
|
|
398
286
|
try {
|
|
399
|
-
if (!extractionEnabled || !identity)
|
|
287
|
+
if (!extractionEnabled || !stores.identity())
|
|
400
288
|
return;
|
|
401
289
|
const personaName = st.lastInjected;
|
|
402
290
|
if (!personaName || !userText)
|
|
@@ -405,14 +293,17 @@ function applyInner(ctx, config = {}) {
|
|
|
405
293
|
// 一条风格约定写回 style_rules(Jaccard 相似自动替换,不堆叠)。冷却与
|
|
406
294
|
// 记忆提取共用,避免同一轮双模型调用。
|
|
407
295
|
if (shouldConsiderCorrection(userText) && !isCoolingDown(st.lastExtractionAt, Date.now(), cooldownMs)) {
|
|
408
|
-
const route = resolveAuxRoute(extractionRouteOverride, llmRoute);
|
|
296
|
+
const route = resolveAuxRoute(extractionRouteOverride, llmRoute.current);
|
|
409
297
|
if (route) {
|
|
410
|
-
const prompt = buildCorrectionPrompt(userText, assistantText,
|
|
298
|
+
const prompt = buildCorrectionPrompt(userText, assistantText, stores
|
|
299
|
+
.identity()
|
|
300
|
+
.getStyleRules(personaName)
|
|
301
|
+
.map((r) => r.rule));
|
|
411
302
|
const output = await callLlm(route, prompt.system, prompt.userText, 400);
|
|
412
303
|
const rule = output === null ? null : parseCorrectionRule(output);
|
|
413
304
|
if (rule) {
|
|
414
305
|
st.lastExtractionAt = Date.now();
|
|
415
|
-
await identity.addStyleRule(personaName, rule, (a, b) => jaccard(a, b) >= 0.6);
|
|
306
|
+
await stores.identity().addStyleRule(personaName, rule, (a, b) => jaccard(a, b) >= 0.6);
|
|
416
307
|
ctx.logger?.warn?.(`lume: 纠偏捕获 → ${personaName}: ${rule}`);
|
|
417
308
|
}
|
|
418
309
|
}
|
|
@@ -420,7 +311,9 @@ function applyInner(ctx, config = {}) {
|
|
|
420
311
|
// 通道 B:语料摘录——用户认可上一轮回复「像本人」时,把真实对话对
|
|
421
312
|
// 摘录进 corpus_pins(注入时并入采样池,让语气随真实使用收敛)。
|
|
422
313
|
if (shouldCaptureCorpus(userText) && pinCandidate && pinCandidate.assistant) {
|
|
423
|
-
const written = await
|
|
314
|
+
const written = await stores
|
|
315
|
+
.identity()
|
|
316
|
+
.addCorpusPin(personaName, { user: pinCandidate.user, assistant: pinCandidate.assistant, at: Date.now() }, (a, b) => jaccard(a, b) >= 0.8);
|
|
424
317
|
if (written)
|
|
425
318
|
ctx.logger?.warn?.(`lume: 语料摘录 → ${personaName}: ${pinCandidate.assistant.slice(0, 40)}`);
|
|
426
319
|
}
|
|
@@ -429,24 +322,26 @@ function applyInner(ctx, config = {}) {
|
|
|
429
322
|
return;
|
|
430
323
|
if (isCoolingDown(st.lastExtractionAt, Date.now(), cooldownMs))
|
|
431
324
|
return;
|
|
432
|
-
const existing = identity.getMemory(personaName);
|
|
325
|
+
const existing = stores.identity().getMemory(personaName);
|
|
433
326
|
if (isDuplicateFact(userText, existing))
|
|
434
327
|
return;
|
|
435
328
|
const prompt = buildExtractionPrompt(userText, assistantText, existing.map((f) => f.text));
|
|
436
|
-
const output = await callLlm(resolveAuxRoute(extractionRouteOverride, llmRoute), prompt.system, prompt.userText, 800);
|
|
437
|
-
if (output === null)
|
|
329
|
+
const output = await callLlm(resolveAuxRoute(extractionRouteOverride, llmRoute.current), prompt.system, prompt.userText, 800);
|
|
330
|
+
if (output === null) {
|
|
331
|
+
ctx.logger?.warn?.(`lume: 反思跳过(${sid})模型无输出`);
|
|
438
332
|
return;
|
|
333
|
+
}
|
|
439
334
|
st.lastExtractionAt = Date.now();
|
|
440
|
-
const fresh = mergeNewFacts(parseFacts(output), identity.getMemory(personaName));
|
|
335
|
+
const fresh = mergeNewFacts(parseFacts(output), stores.identity().getMemory(personaName));
|
|
441
336
|
for (const fact of fresh) {
|
|
442
|
-
const written = await identity.addMemory(personaName, fact, (candidate, all) => isDuplicateFact(candidate, all));
|
|
337
|
+
const written = await stores.identity().addMemory(personaName, fact, (candidate, all) => isDuplicateFact(candidate, all));
|
|
443
338
|
if (written)
|
|
444
339
|
ctx.logger?.warn?.(`lume: 提取记忆 → ${personaName}: ${fact}`);
|
|
445
340
|
}
|
|
446
341
|
// 取名类事实同步身份档案:下拉显示档案名 + 【你是谁】段生效
|
|
447
342
|
const named = extractNaming(fresh);
|
|
448
343
|
if (named) {
|
|
449
|
-
await identity.setProfileName(personaName, named);
|
|
344
|
+
await stores.identity().setProfileName(personaName, named);
|
|
450
345
|
ctx.logger?.warn?.(`lume: 人设 ${personaName} 被命名为「${named}」`);
|
|
451
346
|
}
|
|
452
347
|
}
|
|
@@ -456,13 +351,14 @@ function applyInner(ctx, config = {}) {
|
|
|
456
351
|
}
|
|
457
352
|
/** 蒸馏任务 Runner:素材文本 → 角色卡(契约+语料)。路由可配专用档(distillProvider/Model),默认跟随主对话。 */
|
|
458
353
|
const distillRunner = new DistillJobRunner({
|
|
459
|
-
route: () => resolveAuxRoute(distillRouteOverride, llmRoute),
|
|
354
|
+
route: () => resolveAuxRoute(distillRouteOverride, llmRoute.current),
|
|
460
355
|
call: (route, system, userText, maxTokens, signal) => callLlm(route, system, userText, maxTokens, signal),
|
|
461
356
|
logger: ctx.logger,
|
|
462
357
|
});
|
|
463
358
|
// 版本迁移:有本地原始素材的旧角色在后台自动重蒸馏;只替换基础契约/语料,
|
|
464
359
|
// 身份名、记忆、习得风格与 corpus pins 均留在独立表中,不参与覆盖。
|
|
465
|
-
void identityReady
|
|
360
|
+
void stores.identityReady
|
|
361
|
+
.then(async (store) => {
|
|
466
362
|
if (!store)
|
|
467
363
|
return;
|
|
468
364
|
for (const [personaName, oldCard] of Object.entries(store.listCustomPersonas())) {
|
|
@@ -470,7 +366,7 @@ function applyInner(ctx, config = {}) {
|
|
|
470
366
|
continue;
|
|
471
367
|
try {
|
|
472
368
|
const upgraded = await runDistill({
|
|
473
|
-
route: () => resolveAuxRoute(distillRouteOverride, llmRoute),
|
|
369
|
+
route: () => resolveAuxRoute(distillRouteOverride, llmRoute.current),
|
|
474
370
|
call: (route, system, userText, maxTokens, signal) => callLlm(route, system, userText, maxTokens, signal),
|
|
475
371
|
logger: ctx.logger,
|
|
476
372
|
}, { text: oldCard.distillSource, hint: oldCard.distillHint });
|
|
@@ -490,549 +386,12 @@ function applyInner(ctx, config = {}) {
|
|
|
490
386
|
ctx.logger?.warn?.(`lume: 角色卡 ${personaName} 后台升级失败,保留旧卡`, error);
|
|
491
387
|
}
|
|
492
388
|
}
|
|
493
|
-
})
|
|
389
|
+
})
|
|
390
|
+
.catch((error) => ctx.logger?.warn?.(`lume: 旧角色后台重蒸馏失败:${describeError(error)}`));
|
|
494
391
|
// ── 会话事件:路由缓存 + 轮次缓冲 + 提取调度 + 清理 ──
|
|
495
|
-
ctx.effect(() => ctx.on("session/event", (session, event) =>
|
|
496
|
-
|
|
497
|
-
const st = runtime.get(sid);
|
|
498
|
-
switch (event.type) {
|
|
499
|
-
case "request/context": {
|
|
500
|
-
// 路由缓存的真正来源:agent-loop 在路由变化时 append 的 request/context
|
|
501
|
-
// ({provider, model, contextWindow})。request/header 的载荷是 {header,
|
|
502
|
-
// reason},拿不到 provider/model——v0.3.0 一直监听错了事件,提取从未跑通。
|
|
503
|
-
const data = event.data;
|
|
504
|
-
if (typeof data?.provider === "string" && typeof data?.model === "string") {
|
|
505
|
-
llmRoute = { provider: data.provider, model: data.model };
|
|
506
|
-
ctx.logger?.warn?.(`lume: request/context 更新 llmRoute → ${llmRoute.provider}/${llmRoute.model}`);
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
ctx.logger?.warn?.("lume: request/context 未携带 provider/model,保留 llmRoute", data);
|
|
510
|
-
}
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
case "user/message": {
|
|
514
|
-
// 压缩检查点:宿主把被压缩的历史替换成一条摘要消息,必须与真实
|
|
515
|
-
// 用户消息区分——否则摘要会被当成「用户当前说的话」,污染协议
|
|
516
|
-
// 路由所依赖的 lastQuery 与对话缓冲。这是兜底识别:同一轮里
|
|
517
|
-
// compaction/summary 通常先到且带规模,不要把那条覆盖成无规模的。
|
|
518
|
-
if (isCompactionCheckpoint(event.data)) {
|
|
519
|
-
if (!st.compaction || st.compaction.turnIndex !== st.turnIndex) {
|
|
520
|
-
st.compaction = { turnIndex: st.turnIndex, shadowedItems: 0, tokens: 0 };
|
|
521
|
-
}
|
|
522
|
-
appendLumeLog(`[${sid}] 检测到上下文压缩检查点(第 ${st.turnIndex} 轮)`);
|
|
523
|
-
break;
|
|
524
|
-
}
|
|
525
|
-
// 只有真实用户消息能定义本轮意图。宿主快照(@deepseek-ai/dsh-system-prompt)、
|
|
526
|
-
// 工作区指令(agent-instructions)、技能目录(skill-catalog)都经这条通道投递,
|
|
527
|
-
// 曾被当成用户发言:覆盖真实请求,并把模式从「执行」冲成「问答」。
|
|
528
|
-
if (!isUserAuthored(event.data))
|
|
529
|
-
break;
|
|
530
|
-
const text = messageText(event.data);
|
|
531
|
-
if (text) {
|
|
532
|
-
const normalized = text.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
533
|
-
const explicitCorrection = /不是这个意思|不是我说的|你理解错|答非所问|听不懂|我说的是|我指的是|不对|错了|别这样|重新来/i.test(text);
|
|
534
|
-
const repeatedRequest = normalized.length >= 5 && st.recentUserQueries.includes(normalized);
|
|
535
|
-
st.userText = text;
|
|
536
|
-
st.alignmentCorrection = explicitCorrection
|
|
537
|
-
? buildAlignmentCorrection("user-correction")
|
|
538
|
-
: repeatedRequest
|
|
539
|
-
? buildAlignmentCorrection("repeated-request")
|
|
540
|
-
: null;
|
|
541
|
-
st.recentUserQueries.push(normalized);
|
|
542
|
-
if (st.recentUserQueries.length > 5)
|
|
543
|
-
st.recentUserQueries.shift();
|
|
544
|
-
st.recentTurns.push(`用户: ${text.slice(0, 300)}`);
|
|
545
|
-
if (st.recentTurns.length > 12)
|
|
546
|
-
st.recentTurns.shift();
|
|
547
|
-
// 模式/阶段的冻结统一由 resolveIntent(组装时读会话权威历史)负责,
|
|
548
|
-
// 这里只记账:两处都写会让「同一条消息」被判定为不同轮而反复重算。
|
|
549
|
-
}
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
552
|
-
case "assistant/message": {
|
|
553
|
-
const text = messageText(event.data?.message);
|
|
554
|
-
if (text) {
|
|
555
|
-
st.assistantText = text;
|
|
556
|
-
st.recentTurns.push(`助手: ${text.slice(0, 300)}`);
|
|
557
|
-
if (st.recentTurns.length > 12)
|
|
558
|
-
st.recentTurns.shift();
|
|
559
|
-
}
|
|
560
|
-
break;
|
|
561
|
-
}
|
|
562
|
-
case "tool/call": {
|
|
563
|
-
st.toolCalls++;
|
|
564
|
-
if (st.interactionMode === "execute")
|
|
565
|
-
st.taskPhase = advancePhase(st.taskPhase, "execute");
|
|
566
|
-
// 行为类别在调用阶段记账(连击),成败到结果阶段才结算。
|
|
567
|
-
st.toolKind = classifyTool(event.data?.name);
|
|
568
|
-
applyToolSignal(st.triggerCounters, st.toolKind, null);
|
|
569
|
-
break;
|
|
570
|
-
}
|
|
571
|
-
case "tool/result": {
|
|
572
|
-
const data = event.data;
|
|
573
|
-
const resultText = messageText(data?.message);
|
|
574
|
-
const explicitError = Boolean(data?.error) || /失败|报错|错误|exception|traceback|timed out|permission denied|unknown|not started/i.test(resultText);
|
|
575
|
-
const unknownResult = /结果未知|outcome unknown|tool_not_started|tool_outcome_unknown/i.test(resultText);
|
|
576
|
-
if (unknownResult)
|
|
577
|
-
st.toolUnknown++;
|
|
578
|
-
else if (explicitError)
|
|
579
|
-
st.toolFailures++;
|
|
580
|
-
else
|
|
581
|
-
st.toolSuccesses++;
|
|
582
|
-
if (st.interactionMode === "execute")
|
|
583
|
-
st.taskPhase = advancePhase(st.taskPhase, unknownResult || explicitError ? "diagnose" : "verify");
|
|
584
|
-
// 行为信号 → 计数器 → 触发器提醒。提醒只在这一步之后可见(尾部快照),
|
|
585
|
-
// 且每类触发器有轮级冷却:提示一多就变噪音,模型会学会忽略。
|
|
586
|
-
const signals = readResultSignals(resultText, explicitError);
|
|
587
|
-
applyVerifyOutcome(st.triggerCounters, st.toolKind, signals);
|
|
588
|
-
if (behaviorTriggersOn) {
|
|
589
|
-
const fire = evaluateToolTrigger(st.triggerCounters, {
|
|
590
|
-
turnIndex: st.turnIndex,
|
|
591
|
-
isTask: isTaskQuery(st),
|
|
592
|
-
diagnosing: st.interactionMode === "diagnosis",
|
|
593
|
-
hasContract: contractOf(sid) !== null,
|
|
594
|
-
hasHypotheses: hypothesesOf(sid).length > 0,
|
|
595
|
-
hypothesesTouched: st.hypothesesTouched,
|
|
596
|
-
}, triggerThresholds);
|
|
597
|
-
if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
598
|
-
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
599
|
-
st.triggerNudge = fire.text;
|
|
600
|
-
ctx.logger?.warn?.(`lume: [${sid}] 行为触发器 ${fire.id}(steps=${st.triggerCounters.steps},inspect=${st.triggerCounters.inspectStreak},mutate=${st.triggerCounters.mutateStreak},verifyFail=${st.triggerCounters.verifyFailStreak})`);
|
|
601
|
-
// 环境性死路自动落成项目知识:下次会话不必重踩。
|
|
602
|
-
if (fire.id === "dead-path" && signals.env && project) {
|
|
603
|
-
const key = projectKeyFor(sid, session);
|
|
604
|
-
void project.addFact(key, normalizeProjectFact({ kind: "deadend", text: `本环境验证受阻(${st.triggerCounters.verifyFailStreak} 次连续失败,环境/依赖类):换降级阶梯,不要重复同一命令` }, Date.now()), (candidate, existing) => existing.some((fact) => fact.text === candidate));
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
case "compaction/summary": {
|
|
611
|
-
// 压缩由宿主 preset 在隔离域执行(Lume 无法接管该服务),但事件在
|
|
612
|
-
// 会话总线上可见。记录规模,供下一轮注入「摘要不是完整历史」的重锚。
|
|
613
|
-
const data = event.data;
|
|
614
|
-
const shadowedItems = Array.isArray(data?.shadowedSeqs) ? data.shadowedSeqs.length : 0;
|
|
615
|
-
const tokens = typeof data?.shadowedTokenCount === "number" ? data.shadowedTokenCount : 0;
|
|
616
|
-
st.compaction = { turnIndex: st.turnIndex, shadowedItems, tokens };
|
|
617
|
-
appendLumeLog(`[${sid}] 压缩完成:替换 ${shadowedItems} 项历史(~${tokens} tokens),下一轮注入状态重锚`);
|
|
618
|
-
break;
|
|
619
|
-
}
|
|
620
|
-
case "turn/end": {
|
|
621
|
-
st.turnIndex++;
|
|
622
|
-
// 切换窗口的消耗只发生在轮边界(渲染函数只读状态,不再就地清零):
|
|
623
|
-
// 同一步里 prompt 会被构建多次,若在渲染里消耗窗口,第二次构建就会
|
|
624
|
-
// 丢掉接班招呼——那是「注入随构建次数漂移」,正是本版要消灭的东西。
|
|
625
|
-
st.switchGreetingPending = false;
|
|
626
|
-
if (st.switchTurn !== null && st.turnIndex - st.switchTurn >= boundaryTurns)
|
|
627
|
-
st.switchTurn = null;
|
|
628
|
-
// ── 行为触发器(轮边界)──
|
|
629
|
-
// 连击按轮清零:新一轮是新请求,上一轮的「撒网」不该继续累加;死路连击
|
|
630
|
-
// 跨轮保留(同一环境不可用是会话级事实)。上轮的提醒到这里失效。
|
|
631
|
-
st.triggerCounters.inspectStreak = 0;
|
|
632
|
-
st.triggerCounters.mutateStreak = 0;
|
|
633
|
-
st.triggerNudge = null;
|
|
634
|
-
st.turnNudge = null;
|
|
635
|
-
st.hypothesesTouched = false;
|
|
636
|
-
if (behaviorTriggersOn && project) {
|
|
637
|
-
const fire = evaluateTurnTrigger({
|
|
638
|
-
turnIndex: st.turnIndex,
|
|
639
|
-
hasContract: contractOf(sid) !== null,
|
|
640
|
-
compactionTurn: st.compaction?.turnIndex ?? null,
|
|
641
|
-
lastDriftTurn: st.lastDriftTurn,
|
|
642
|
-
counters: st.triggerCounters,
|
|
643
|
-
knowledgePrompted: st.knowledgePrompted,
|
|
644
|
-
}, triggerThresholds);
|
|
645
|
-
if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
646
|
-
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
647
|
-
if (fire.id === "criteria-drift") {
|
|
648
|
-
// 契约对账用「交付口径」渲染原始判据:防判据随进展漂移。
|
|
649
|
-
st.lastDriftTurn = st.turnIndex;
|
|
650
|
-
st.turnNudge = renderContract(contractOf(sid), true);
|
|
651
|
-
}
|
|
652
|
-
else {
|
|
653
|
-
st.knowledgePrompted = true;
|
|
654
|
-
st.turnNudge = fire.text;
|
|
655
|
-
}
|
|
656
|
-
ctx.logger?.warn?.(`lume: [${sid}] 轮触发器 ${fire.id}(turn=${st.turnIndex})`);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
// 低成本会话内纠偏:只处理明确的错误/失败信号,且要求连续轮次用户请求相同。
|
|
660
|
-
const failed = /失败|报错|错误|exception|traceback|cannot|unable|permission denied|timed out|找不到|不存在/i.test(st.assistantText);
|
|
661
|
-
const queryKey = st.userText.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
662
|
-
if (failed && queryKey && queryKey === st.lastFailureQuery)
|
|
663
|
-
st.failureStreak++;
|
|
664
|
-
else if (failed && queryKey) {
|
|
665
|
-
st.lastFailureQuery = queryKey;
|
|
666
|
-
st.failureStreak = 1;
|
|
667
|
-
}
|
|
668
|
-
else if (!failed) {
|
|
669
|
-
st.failureStreak = 0;
|
|
670
|
-
st.lastFailureQuery = null;
|
|
671
|
-
st.protocolCorrection = null;
|
|
672
|
-
}
|
|
673
|
-
if (st.failureStreak >= 2)
|
|
674
|
-
st.protocolCorrection = "检测到相同请求连续失败:先定位根因并记录已排除假设,再选择不同方案;不要重复同一调用。";
|
|
675
|
-
const claimsVerification = /验证|测试|构建|检查|确认生效|实际结果|已通过|未验证|无法验证/i.test(st.assistantText);
|
|
676
|
-
st.postTurnReview = st.interactionMode === "execute" && st.assistantText && !claimsVerification
|
|
677
|
-
? "〔上轮交付复核〕上一轮执行回复没有给出可见的验证证据。本轮若继续处理同一任务,先确认上轮变更是否真实生效,再继续扩大范围。"
|
|
678
|
-
: null;
|
|
679
|
-
if (st.interactionMode === "execute")
|
|
680
|
-
st.taskPhase = advancePhase(st.taskPhase, st.toolFailures > 0 || st.toolUnknown > 0 ? "diagnose" : claimsVerification ? "deliver" : "verify");
|
|
681
|
-
// 即时对齐只影响当前轮;下一轮重新根据用户消息判断,避免纠偏条款滞留。
|
|
682
|
-
st.alignmentCorrection = null;
|
|
683
|
-
// 风格泄漏检测挂在 turn/end(该事件已被窗口机制验证可靠;assistant/message
|
|
684
|
-
// 的投递在实测中不可靠)。切换完成后逐轮检查回复是否残留旧人设签名词,
|
|
685
|
-
// 窗口已关仍检出 → 重开窗口 + 升级播报;一轮干净回复自动解除升级。
|
|
686
|
-
if (st.prevSignatures.length > 0 && st.lastInjected !== undefined && st.assistantText) {
|
|
687
|
-
const report = detectLeak(st.assistantText, st.prevSignatures);
|
|
688
|
-
const inWindow = st.switchTurn !== null && st.turnIndex - st.switchTurn < boundaryTurns;
|
|
689
|
-
if (report.leaked && !inWindow) {
|
|
690
|
-
st.switchTurn = st.turnIndex;
|
|
691
|
-
st.leakEscalated = true;
|
|
692
|
-
ctx.logger?.warn?.(`lume: [${sid}] 检测到旧人设风格泄漏(${report.hits.map((h) => `${h.word}×${h.count}`).join("、")}),重新注入升级版切换播报`);
|
|
693
|
-
}
|
|
694
|
-
else if (!report.leaked) {
|
|
695
|
-
st.leakEscalated = false;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
scheduleExtraction(sid, st);
|
|
699
|
-
break;
|
|
700
|
-
}
|
|
701
|
-
default:
|
|
702
|
-
// 诊断:压缩事件是否经由 session/event 总线投递(宿主按 session
|
|
703
|
-
// 所属上下文收集监听者,隔离域里发出的日志事件可能不经过这里)。
|
|
704
|
-
if (typeof event.type === "string" && /compact/i.test(event.type)) {
|
|
705
|
-
appendLumeLog(`[${sid}] 收到未处理的压缩事件类型 ${event.type}`);
|
|
706
|
-
}
|
|
707
|
-
break;
|
|
708
|
-
}
|
|
709
|
-
}), "lume: session events");
|
|
710
|
-
ctx.effect(() => ctx.on("session/disposed", (session) => {
|
|
711
|
-
const sid = String(session.id);
|
|
712
|
-
const st = runtime.get(sid);
|
|
713
|
-
const turns = [...st.recentTurns];
|
|
714
|
-
runtime.delete(sid);
|
|
715
|
-
// 任务载具是会话态:任务结束即无意义,清掉避免无界增长(项目知识在另一张表,不受影响)。
|
|
716
|
-
void projectReady.then((store) => store?.clearSession(sid));
|
|
717
|
-
// 反思日志:会话结束后空闲时间跑一次小模型,零用户感知 token。
|
|
718
|
-
// 历史不够长(< 4 条消息)或路由不可用时静默跳过。
|
|
719
|
-
if (reflectionEnabled && turns.length >= 4) {
|
|
720
|
-
void (async () => {
|
|
721
|
-
const store = await reflectionReady;
|
|
722
|
-
if (!store)
|
|
723
|
-
return;
|
|
724
|
-
const route = resolveAuxRoute({}, llmRoute);
|
|
725
|
-
if (!route)
|
|
726
|
-
return;
|
|
727
|
-
const prompt = buildReflectionPrompt(turns);
|
|
728
|
-
const output = await callLlm(route, prompt.system, prompt.userText, 800);
|
|
729
|
-
if (output === null)
|
|
730
|
-
return;
|
|
731
|
-
const score = parseReflectionScore(output);
|
|
732
|
-
if (!score)
|
|
733
|
-
return;
|
|
734
|
-
await store.log(sid, score);
|
|
735
|
-
ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review} diagnosis=${score.diagnosis}「${score.note}」`);
|
|
736
|
-
})();
|
|
737
|
-
}
|
|
738
|
-
}), "lume: session disposal");
|
|
392
|
+
ctx.effect(() => ctx.on("session/event", (session, event) => sessionEventHandler(session, event)), "lume: session events");
|
|
393
|
+
ctx.effect(() => ctx.on("session/disposed", (session) => sessionDisposedHandler(session)), "lume: session disposal");
|
|
739
394
|
// ── 载具与项目知识的读取入口(事件处理器 / 工具 / 注入三处共用)──
|
|
740
|
-
/** 项目键:优先取会话工作目录(跨会话共享同一仓库的知识)。 */
|
|
741
|
-
function projectKeyFor(sid, source) {
|
|
742
|
-
const st = runtime.get(sid);
|
|
743
|
-
if (st.projectKey)
|
|
744
|
-
return st.projectKey;
|
|
745
|
-
// 三种调用来源:提示词 context({agent:{session}})、工具 exec({agent:{session}})、
|
|
746
|
-
// 会话事件(session 本身)。统一取到 session 再读 cwd。
|
|
747
|
-
const session = source?.agent?.session ?? source?.session ?? source;
|
|
748
|
-
const cwd = session?.cwd ?? "";
|
|
749
|
-
const key = projectKeyOf(cwd);
|
|
750
|
-
// 只有拿到真实工作目录才缓存:否则一次无 cwd 的调用会把 "unknown" 固化下来。
|
|
751
|
-
if (cwd)
|
|
752
|
-
st.projectKey = key;
|
|
753
|
-
return key;
|
|
754
|
-
}
|
|
755
|
-
function isTaskQuery(st) {
|
|
756
|
-
return TASK_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? "");
|
|
757
|
-
}
|
|
758
|
-
function contractOf(sid) {
|
|
759
|
-
return project?.getContract(sid) ?? null;
|
|
760
|
-
}
|
|
761
|
-
function changesOf(sid) {
|
|
762
|
-
return project?.getChanges(sid) ?? [];
|
|
763
|
-
}
|
|
764
|
-
function hypothesesOf(sid) {
|
|
765
|
-
return project?.getHypotheses(sid) ?? [];
|
|
766
|
-
}
|
|
767
|
-
function factsOf(sid, context) {
|
|
768
|
-
return project ? project.getFacts(projectKeyFor(sid, context)) : [];
|
|
769
|
-
}
|
|
770
|
-
/** 环境里是否有符号级结构分析工具:有就让模型用它替代通篇 read。 */
|
|
771
|
-
function structureToolName(context) {
|
|
772
|
-
try {
|
|
773
|
-
const schemas = ctx.get("tools")?.schemas?.(context?.agent);
|
|
774
|
-
if (!Array.isArray(schemas))
|
|
775
|
-
return null;
|
|
776
|
-
for (const schema of schemas) {
|
|
777
|
-
const name = String(schema?.name ?? "");
|
|
778
|
-
if (/analy|tree|symbol|lsp|reference|code_map|outline/i.test(name))
|
|
779
|
-
return name;
|
|
780
|
-
}
|
|
781
|
-
return null;
|
|
782
|
-
}
|
|
783
|
-
catch {
|
|
784
|
-
return null;
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* 任务载具块:契约 / 改动台账 / 假设台账 / 项目知识 + 方法块 + 触发器提醒。
|
|
789
|
-
*
|
|
790
|
-
* 全部落在尾部快照(易变层)——这正是「载具」现在才做得起的原因:0.6.2 之前每步
|
|
791
|
-
* 注入一份会变的状态等于每步作废整段前缀,而快照只在内容变化时才付费(实测 58 步
|
|
792
|
-
* 只产生 9 条快照)。顺序上把「此刻最该做的一件事」(触发器提醒)放在最后。
|
|
793
|
-
*/
|
|
794
|
-
function carrierBlocks(sid, context, st, query, mode) {
|
|
795
|
-
if (!projectMemoryOn)
|
|
796
|
-
return [];
|
|
797
|
-
const isTask = TASK_SIGNAL_RE.test(query);
|
|
798
|
-
const contract = contractOf(sid);
|
|
799
|
-
const changes = changesOf(sid);
|
|
800
|
-
const docDirective = buildDocumentDirective({ query, capabilities: probeDocumentCapabilities(ctx.get("tools"), context?.agent) });
|
|
801
|
-
return [
|
|
802
|
-
// 契约:有就回显(交付轮切成对账口径),没有且是任务轮就先教它写一份。
|
|
803
|
-
{ text: renderContract(contract, st.taskPhase === "deliver") },
|
|
804
|
-
{ text: !contract && isTask ? buildContractMethodDirective() : null, droppable: true },
|
|
805
|
-
// 台账与假设:存在就回显——让模型「看见」自己的计划,而不是记在脑子里。
|
|
806
|
-
{ text: changes.length > 0 ? renderChangeLedger(changes) : null },
|
|
807
|
-
{ text: renderHypotheses(hypothesesOf(sid)) },
|
|
808
|
-
// 项目知识:只在与项目相关的轮次出现(闲聊不该背仓库事实)。
|
|
809
|
-
{ text: isTask ? renderProjectFacts(factsOf(sid, context)) : null, droppable: true },
|
|
810
|
-
// 方法块:按任务形态出现;文档方法论只在判定为文档任务时出现。
|
|
811
|
-
{ text: isTask && mode !== "question" ? buildImpactDirective() : null, droppable: true },
|
|
812
|
-
{ text: docDirective ? buildDocumentMethodDirective() : null },
|
|
813
|
-
{ text: mode === "execute" || mode === "diagnosis" ? buildStructureHint(structureToolName(context)) : null, droppable: true },
|
|
814
|
-
{ text: st.turnNudge },
|
|
815
|
-
{ text: st.triggerNudge },
|
|
816
|
-
];
|
|
817
|
-
}
|
|
818
|
-
// ── 模型可调用工具(主写入通道)──
|
|
819
|
-
// 工具 output schema 的 const 语义要求成功值恒为 { ok: true };失败一律抛错交由框架呈现。
|
|
820
|
-
// as const 让 defineTool 从字面量推断 O,三个工具共用同一份成功形状。
|
|
821
|
-
const OK_OUTPUT_SCHEMA = {
|
|
822
|
-
type: "object",
|
|
823
|
-
additionalProperties: false,
|
|
824
|
-
properties: { ok: { type: "boolean", const: true, required: true } },
|
|
825
|
-
};
|
|
826
|
-
function dutyPersona(exec) {
|
|
827
|
-
const sid = exec?.agent?.session?.id;
|
|
828
|
-
const st = sid !== undefined ? runtime.get(String(sid)) : undefined;
|
|
829
|
-
return st?.lastInjected ?? defaultName;
|
|
830
|
-
}
|
|
831
|
-
ctx.effect(() => {
|
|
832
|
-
ctx.tools.register(defineTool({
|
|
833
|
-
name: "lume_remember",
|
|
834
|
-
description: "记住关于用户或你们关系的持久事实(偏好、习惯、背景、称呼)。仅当信息明确值得长期记住时调用;每次一条,40 字以内。不要记录工作内容、代码或项目机密。",
|
|
835
|
-
parameters: {
|
|
836
|
-
text: { type: "string", required: true, description: "要长期记住的事实,第三人称陈述句,≤40 字" },
|
|
837
|
-
},
|
|
838
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
|
|
839
|
-
execute: async (args, exec) => {
|
|
840
|
-
if (!identity)
|
|
841
|
-
throw new Error("lume identity store is unavailable");
|
|
842
|
-
const personaName = dutyPersona(exec);
|
|
843
|
-
if (!personaName)
|
|
844
|
-
throw new Error("lume_remember requires an active persona (当前没有当值人设)");
|
|
845
|
-
await identity.addMemory(personaName, String(args.text), isDuplicateFact);
|
|
846
|
-
return { ok: true };
|
|
847
|
-
},
|
|
848
|
-
}));
|
|
849
|
-
ctx.tools.register(defineTool({
|
|
850
|
-
name: "lume_update_style",
|
|
851
|
-
description: "把用户对你说话方式的新要求固化为长期风格约定(如「少用 emoji」「自称改成XX」)。仅当用户明确提出风格/语气要求时调用,每条一句话。",
|
|
852
|
-
parameters: {
|
|
853
|
-
rule: { type: "string", required: true, description: "风格约定,一句话祈使句" },
|
|
854
|
-
},
|
|
855
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
|
|
856
|
-
execute: async (args, exec) => {
|
|
857
|
-
if (!identity)
|
|
858
|
-
throw new Error("lume identity store is unavailable");
|
|
859
|
-
const personaName = dutyPersona(exec);
|
|
860
|
-
if (!personaName)
|
|
861
|
-
throw new Error("lume_update_style requires an active persona (当前没有当值人设)");
|
|
862
|
-
await identity.addStyleRule(personaName, String(args.rule), (a, b) => jaccard(a, b) >= 0.6);
|
|
863
|
-
return { ok: true };
|
|
864
|
-
},
|
|
865
|
-
}));
|
|
866
|
-
ctx.tools.register(defineTool({
|
|
867
|
-
name: "lume_create_persona",
|
|
868
|
-
description: "创建一个全新的自定义人设。仅当用户明确想新建人设时使用:先在对话中访谈收集(人设的名字、性格、说话方式、对用户的称呼),收集完整后再调用本工具保存,并告知用户保存成功。",
|
|
869
|
-
parameters: {
|
|
870
|
-
name: { type: "string", required: true, description: "人设英文键名,小写字母开头,≤32 字符(如 tsundere)" },
|
|
871
|
-
displayName: { type: "string", required: true, description: "界面显示名(如「傲娇」)" },
|
|
872
|
-
description: { type: "string", required: true, description: "一句话简介" },
|
|
873
|
-
promptText: { type: "string", required: true, description: "完整风格契约:称呼/emoji/语气词/节奏/立场,与内置契约同构" },
|
|
874
|
-
},
|
|
875
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已保存" }] },
|
|
876
|
-
execute: async (args) => {
|
|
877
|
-
if (!identity)
|
|
878
|
-
throw new Error("lume identity store is unavailable");
|
|
879
|
-
await identity.setCustomPersona(String(args.name), {
|
|
880
|
-
displayName: String(args.displayName),
|
|
881
|
-
description: String(args.description ?? ""),
|
|
882
|
-
promptText: String(args.promptText),
|
|
883
|
-
createdAt: Date.now(),
|
|
884
|
-
});
|
|
885
|
-
return { ok: true };
|
|
886
|
-
},
|
|
887
|
-
}));
|
|
888
|
-
// ── 任务载具工具(第二组写入通道)──
|
|
889
|
-
// 与人格工具一样是「模型主动调用、零额外 LLM 调用」,区别在写入对象:契约/台账/假设
|
|
890
|
-
// 属于当前任务(会话态),项目知识按工作目录跨会话累积。列表类参数统一用字符串
|
|
891
|
-
// 分隔(分号或换行),不引入数组 schema——省 schema token,也少一层校验风险。
|
|
892
|
-
const splitList = (value) => String(value ?? "")
|
|
893
|
-
.split(/[;;\n]/)
|
|
894
|
-
.map((item) => item.trim())
|
|
895
|
-
.filter(Boolean);
|
|
896
|
-
ctx.effect(() => {
|
|
897
|
-
ctx.tools.register(defineTool({
|
|
898
|
-
name: "lume_contract",
|
|
899
|
-
description: "写下或更新本任务的任务契约(需求量化的落点):目标、范围、数量、完成判据、非目标、待确认。任务型请求开工前调用一次;探索后回填实际数量;之后只传变化的字段即可(局部更新)。",
|
|
900
|
-
parameters: {
|
|
901
|
-
goal: { type: "string", description: "目标:一句话、可观察的结果" },
|
|
902
|
-
scope: { type: "string", description: "范围:路径/模块/章节,分号或换行分隔" },
|
|
903
|
-
expectCount: { type: "number", description: "预计数量(探索前先估)" },
|
|
904
|
-
actualCount: { type: "number", description: "实际数量(探索后回填)" },
|
|
905
|
-
criteria: { type: "string", description: "完成判据:可执行、可核对,分号或换行分隔" },
|
|
906
|
-
nonGoals: { type: "string", description: "非目标:明确不动的东西,分号分隔" },
|
|
907
|
-
open: { type: "string", description: "待确认:只列真正阻塞的(≤2 个),分号分隔" },
|
|
908
|
-
},
|
|
909
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记录任务契约" }] },
|
|
910
|
-
execute: async (args, exec) => {
|
|
911
|
-
if (!project)
|
|
912
|
-
throw new Error("lume project store is unavailable");
|
|
913
|
-
const sid = String(exec?.agent?.session?.id ?? "");
|
|
914
|
-
if (!sid)
|
|
915
|
-
throw new Error("lume_contract requires an active session");
|
|
916
|
-
const st = runtime.get(sid);
|
|
917
|
-
const normalized = normalizeContract({
|
|
918
|
-
goal: args.goal,
|
|
919
|
-
scope: splitList(args.scope),
|
|
920
|
-
expectCount: args.expectCount,
|
|
921
|
-
actualCount: args.actualCount,
|
|
922
|
-
criteria: splitList(args.criteria),
|
|
923
|
-
nonGoals: splitList(args.nonGoals),
|
|
924
|
-
open: splitList(args.open),
|
|
925
|
-
}, Date.now(), st.turnIndex);
|
|
926
|
-
const existing = project.getContract(sid);
|
|
927
|
-
if (existing) {
|
|
928
|
-
// 局部更新:未传的字段保持原值(回填数量时不该把判据清空)。
|
|
929
|
-
const patch = {};
|
|
930
|
-
if (args.goal !== undefined)
|
|
931
|
-
patch.goal = normalized.goal;
|
|
932
|
-
if (args.scope !== undefined)
|
|
933
|
-
patch.scope = normalized.scope;
|
|
934
|
-
if (args.expectCount !== undefined)
|
|
935
|
-
patch.expectCount = normalized.expectCount;
|
|
936
|
-
if (args.actualCount !== undefined)
|
|
937
|
-
patch.actualCount = normalized.actualCount;
|
|
938
|
-
if (args.criteria !== undefined)
|
|
939
|
-
patch.criteria = normalized.criteria;
|
|
940
|
-
if (args.nonGoals !== undefined)
|
|
941
|
-
patch.nonGoals = normalized.nonGoals;
|
|
942
|
-
if (args.open !== undefined)
|
|
943
|
-
patch.open = normalized.open;
|
|
944
|
-
await project.patchContract(sid, patch);
|
|
945
|
-
}
|
|
946
|
-
else {
|
|
947
|
-
if (!normalized.goal)
|
|
948
|
-
throw new Error("lume_contract requires a goal on first write");
|
|
949
|
-
await project.setContract(sid, normalized);
|
|
950
|
-
}
|
|
951
|
-
return { ok: true };
|
|
952
|
-
},
|
|
953
|
-
}));
|
|
954
|
-
ctx.tools.register(defineTool({
|
|
955
|
-
name: "lume_change",
|
|
956
|
-
description: "改动台账:记录/更新一处将要改或已改的位置(文件/符号/文档章节 → 改什么 → 怎么验 → 状态)。动手前先列计划项,改完推进状态;只推进状态时可只传 target + status。文档任务用章节名当 target,形成分节记账。",
|
|
957
|
-
parameters: {
|
|
958
|
-
target: { type: "string", required: true, description: "目标位置:文件路径 / 符号 / 文档章节" },
|
|
959
|
-
change: { type: "string", description: "改什么(一句话)" },
|
|
960
|
-
why: { type: "string", description: "为什么改(对齐契约的哪一条)" },
|
|
961
|
-
verify: { type: "string", description: "怎么验(命令 / 回读 / 对照)" },
|
|
962
|
-
status: { type: "string", description: "planned | done | verified | skipped" },
|
|
963
|
-
},
|
|
964
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新改动台账" }] },
|
|
965
|
-
execute: async (args, exec) => {
|
|
966
|
-
if (!project)
|
|
967
|
-
throw new Error("lume project store is unavailable");
|
|
968
|
-
const sid = String(exec?.agent?.session?.id ?? "");
|
|
969
|
-
if (!sid)
|
|
970
|
-
throw new Error("lume_change requires an active session");
|
|
971
|
-
const target = String(args.target ?? "").trim();
|
|
972
|
-
if (!target)
|
|
973
|
-
throw new Error("lume_change requires a target");
|
|
974
|
-
const status = args.status;
|
|
975
|
-
const allowed = status === "planned" || status === "done" || status === "verified" || status === "skipped" ? status : undefined;
|
|
976
|
-
if (args.change === undefined && allowed !== undefined) {
|
|
977
|
-
const hit = await project.setChangeStatus(sid, target, allowed);
|
|
978
|
-
if (!hit)
|
|
979
|
-
throw new Error(`lume_change: no ledger entry for ${target}`);
|
|
980
|
-
return { ok: true };
|
|
981
|
-
}
|
|
982
|
-
const item = normalizeChange({ target, change: args.change, why: args.why, verify: args.verify, status: allowed }, Date.now());
|
|
983
|
-
if (!item)
|
|
984
|
-
throw new Error("lume_change requires target and change");
|
|
985
|
-
await project.upsertChange(sid, item);
|
|
986
|
-
return { ok: true };
|
|
987
|
-
},
|
|
988
|
-
}));
|
|
989
|
-
ctx.tools.register(defineTool({
|
|
990
|
-
name: "lume_hypothesis",
|
|
991
|
-
description: "假设台账:记录一条正在验证的假设及其证据与状态(open/testing/confirmed/excluded)。排查类任务里每验证一次就更新状态;已排除的假设不要再重复尝试。",
|
|
992
|
-
parameters: {
|
|
993
|
-
text: { type: "string", required: true, description: "假设内容,一句话" },
|
|
994
|
-
evidence: { type: "string", description: "支持或推翻它的观察(含命令输出/时间戳摘要)" },
|
|
995
|
-
status: { type: "string", description: "open | testing | confirmed | excluded" },
|
|
996
|
-
},
|
|
997
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新假设台账" }] },
|
|
998
|
-
execute: async (args, exec) => {
|
|
999
|
-
if (!project)
|
|
1000
|
-
throw new Error("lume project store is unavailable");
|
|
1001
|
-
const sid = String(exec?.agent?.session?.id ?? "");
|
|
1002
|
-
if (!sid)
|
|
1003
|
-
throw new Error("lume_hypothesis requires an active session");
|
|
1004
|
-
const item = normalizeHypothesis({ text: args.text, evidence: args.evidence, status: args.status }, Date.now());
|
|
1005
|
-
if (!item)
|
|
1006
|
-
throw new Error("lume_hypothesis requires text");
|
|
1007
|
-
await project.upsertHypothesis(sid, item);
|
|
1008
|
-
runtime.get(sid).hypothesesTouched = true;
|
|
1009
|
-
return { ok: true };
|
|
1010
|
-
},
|
|
1011
|
-
}));
|
|
1012
|
-
ctx.tools.register(defineTool({
|
|
1013
|
-
name: "lume_project_note",
|
|
1014
|
-
description: "记录一条**稳定的项目事实**(按工作目录跨会话累积):构建/测试命令、模块数据流、仓库约定、或一条死路(试过但行不通的做法)。只记可复用、已验证的事实,不要记一次性进展。",
|
|
1015
|
-
parameters: {
|
|
1016
|
-
kind: { type: "string", required: true, description: "build | test | module | convention | deadend" },
|
|
1017
|
-
text: { type: "string", required: true, description: "事实本身,一句话,≤200 字" },
|
|
1018
|
-
},
|
|
1019
|
-
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记入项目知识" }] },
|
|
1020
|
-
execute: async (args, exec) => {
|
|
1021
|
-
if (!project)
|
|
1022
|
-
throw new Error("lume project store is unavailable");
|
|
1023
|
-
const sid = String(exec?.agent?.session?.id ?? "");
|
|
1024
|
-
if (!sid)
|
|
1025
|
-
throw new Error("lume_project_note requires an active session");
|
|
1026
|
-
const fact = normalizeProjectFact({ kind: args.kind, text: args.text }, Date.now());
|
|
1027
|
-
if (!fact)
|
|
1028
|
-
throw new Error("lume_project_note requires text");
|
|
1029
|
-
await project.addFact(projectKeyFor(sid, { agent: exec?.agent }), fact, (candidate, existing) => existing.some((entry) => jaccard(entry.text, candidate) >= 0.7));
|
|
1030
|
-
return { ok: true };
|
|
1031
|
-
},
|
|
1032
|
-
}));
|
|
1033
|
-
}, "lume: carrier tools");
|
|
1034
|
-
}, "lume: persona tools");
|
|
1035
|
-
// ── 人设五段式注入 + 切换播报 ──
|
|
1036
395
|
/**
|
|
1037
396
|
* 权威意图解析:与宿主会话历史对账,而不是只信事件缓存。
|
|
1038
397
|
*
|
|
@@ -1041,8 +400,10 @@ function applyInner(ctx, config = {}) {
|
|
|
1041
400
|
* 读会话投影出的消息(`deriveMessages`,宿主侧带增量缓存),取最后一条真实用户
|
|
1042
401
|
* 消息作为本轮意图;messageId 未变时零成本短路,保证一轮内只冻结一次。
|
|
1043
402
|
*/
|
|
1044
|
-
function resolveIntent(context, st) {
|
|
403
|
+
function resolveIntent(sid, context, st) {
|
|
1045
404
|
const session = context?.agent?.session;
|
|
405
|
+
if (typeof session?.cwd === "string" && session.cwd)
|
|
406
|
+
st.cwd = session.cwd;
|
|
1046
407
|
const messages = typeof session?.deriveMessages === "function" ? session.deriveMessages() : [];
|
|
1047
408
|
let text = null;
|
|
1048
409
|
let messageId = "";
|
|
@@ -1064,7 +425,20 @@ function applyInner(ctx, config = {}) {
|
|
|
1064
425
|
messageId = `text:${st.userText.slice(0, 120)}`;
|
|
1065
426
|
}
|
|
1066
427
|
if (text !== null && (st.intent === null || st.intent.messageId !== messageId)) {
|
|
1067
|
-
|
|
428
|
+
// 路由从「这一句话的词」升到「最近几轮轨迹的证据」(0.8.x):纠正当场按被纠正前那句话重算、
|
|
429
|
+
// 在途任务对承接式追问有粘性、任务型轨迹上不因一句没有动词的话掉回问答。
|
|
430
|
+
// 路由是唯一「判错就全盘错」的决策,所以判定结论(模式 + 命中判据 + 证据来源)一并落进度量。
|
|
431
|
+
// recentUserQueries 里**已经包含这一句**(user/message 事件先到):轨迹证据必须把它剔掉,
|
|
432
|
+
// 否则「最近三轮有两轮任务型」会被当前句自己凑够——那是自证,不是证据。
|
|
433
|
+
const normalizedQuery = text.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
434
|
+
const decision = classifyWithTrajectory({
|
|
435
|
+
text,
|
|
436
|
+
recentUserTexts: st.recentUserQueries.filter((item) => item !== normalizedQuery),
|
|
437
|
+
prevMode: st.interactionMode,
|
|
438
|
+
prevPhase: st.taskPhase,
|
|
439
|
+
hadMutations: st.triggerCounters.mutations > 0,
|
|
440
|
+
});
|
|
441
|
+
const mode = decision.mode;
|
|
1068
442
|
st.intent = { turnIndex: st.turnIndex, messageId, text };
|
|
1069
443
|
st.lastQuery = text;
|
|
1070
444
|
st.interactionMode = mode;
|
|
@@ -1073,41 +447,207 @@ function applyInner(ctx, config = {}) {
|
|
|
1073
447
|
st.toolSuccesses = 0;
|
|
1074
448
|
st.toolFailures = 0;
|
|
1075
449
|
st.toolUnknown = 0;
|
|
450
|
+
metricsLog.record({
|
|
451
|
+
kind: "route",
|
|
452
|
+
at: Date.now(),
|
|
453
|
+
sid,
|
|
454
|
+
turn: st.turnIndex,
|
|
455
|
+
mode: decision.mode,
|
|
456
|
+
matched: decision.matched,
|
|
457
|
+
source: decision.source,
|
|
458
|
+
excerpt: text.trim().replace(/\s+/g, " ").slice(0, 120),
|
|
459
|
+
...(decision.evidence ? { evidence: decision.evidence.trim().replace(/\s+/g, " ").slice(0, 120) } : {}),
|
|
460
|
+
});
|
|
1076
461
|
}
|
|
1077
462
|
return { text: st.intent?.text ?? st.lastQuery ?? "", mode: st.interactionMode };
|
|
1078
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* 本步的注入文本,按「会话恒定 / 每步易变」分成四段。
|
|
466
|
+
*
|
|
467
|
+
* 恒定段必须在一个会话内逐字节不变——它排在消息序列最前面,变一个字就作废它
|
|
468
|
+
* 后面的全部内容(工具定义、结构化输出、**整段对话历史**)。所以恒定段只允许
|
|
469
|
+
* 依赖会话级状态:当值人设、契约正文、身份名、模型能力。任何按 query / 轮次 /
|
|
470
|
+
* 阶段变化的内容都归易变段。
|
|
471
|
+
*/
|
|
472
|
+
/** 提示块装配的依赖:块表在 host/prompt-blocks.ts,这里只负责接线。 */
|
|
473
|
+
/** 会话事件处理器的依赖(处理器本体在 host/session-events.ts,这里只接线)。 */
|
|
474
|
+
/** disposed 处理器复用同一批依赖。 */
|
|
475
|
+
// ── 运行时度量(0.8.x)──
|
|
476
|
+
// 放在装配最前面:它是后面所有优化的**靶子**——没有度量,「路由改准了没有 / 条款加权有没有用」
|
|
477
|
+
// 都只能靠感觉(这正是上一轮改动没能证明自己变聪明的原因)。
|
|
478
|
+
const metricsLog = createMetricsLog({ enabled: metricsEnabled });
|
|
479
|
+
// 回读磁盘尾部:跨会话趋势(纠正率是升还是降)比单个会话更有信息量。
|
|
480
|
+
metricsLog.loadFromDisk();
|
|
481
|
+
// ── 依赖装配:三个 deps 对象集中在 host/wiring.ts(无状态函数由它自己 import)──
|
|
482
|
+
// 这里只传「index 才有的运行期状态」:ctx / 存储句柄 / 会话态 / 模型路由 / 项目访问入口 / 配置开关。
|
|
483
|
+
const wiring = {
|
|
484
|
+
ctx,
|
|
485
|
+
runtime,
|
|
486
|
+
stores: {
|
|
487
|
+
identity: () => stores.identity(),
|
|
488
|
+
project: () => stores.project(),
|
|
489
|
+
reflectionReady: stores.reflectionReady,
|
|
490
|
+
},
|
|
491
|
+
access: {
|
|
492
|
+
contractOf,
|
|
493
|
+
changesOf,
|
|
494
|
+
designOf,
|
|
495
|
+
requirementsOf,
|
|
496
|
+
hypothesesOf,
|
|
497
|
+
factsOf,
|
|
498
|
+
projectKeyFor,
|
|
499
|
+
flushPendingFacts,
|
|
500
|
+
settleVerification,
|
|
501
|
+
saveSessionMemory,
|
|
502
|
+
taskMemoriesOf,
|
|
503
|
+
needsDesignPass,
|
|
504
|
+
structureToolName,
|
|
505
|
+
},
|
|
506
|
+
projectTask: stores.projectTask,
|
|
507
|
+
llmRoute,
|
|
508
|
+
callLlm,
|
|
509
|
+
defaultName,
|
|
510
|
+
isTaskQuery,
|
|
511
|
+
scheduleExtraction,
|
|
512
|
+
triggerThresholds,
|
|
513
|
+
boundaryTurns,
|
|
514
|
+
taskSignalRe: TASK_SIGNAL_RE,
|
|
515
|
+
docArtifactRe: DOC_ARTIFACT_RE,
|
|
516
|
+
probeCaps: (context) => probeDocumentCapabilities(structureToolName(context), context),
|
|
517
|
+
reflectionFeedback: () => stores.reflectionStore()?.getFeedback() ?? null,
|
|
518
|
+
projectMemoryOn,
|
|
519
|
+
behaviorTriggersOn,
|
|
520
|
+
reflectionEnabled,
|
|
521
|
+
metrics: {
|
|
522
|
+
record: (record) => metricsLog.record(record),
|
|
523
|
+
summary: (scope) => metricsLog.summaryText(scope),
|
|
524
|
+
health: (sid) => metricsLog.health(sid),
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
// 会话补蒸馏:把最近 7 天的会话(**含已经撑满、聊不动的那些**)榨成跨会话知识。
|
|
528
|
+
// 为什么必须在这里做:上下文撑满 → 宿主压缩会失败(现场:compaction/end error=context overflow)→
|
|
529
|
+
// 会话再也产不出事件 → 期间没沉淀的知识会永久丢;但会话记录还躺在硬盘上。
|
|
530
|
+
// 分片执行(每片一个会话)以免阻塞宿主同进程的事件循环;幂等来自 addFact 的相似度去重。
|
|
531
|
+
ctx.effect(() => {
|
|
532
|
+
let stopBackfill = null;
|
|
533
|
+
void stores.projectReady
|
|
534
|
+
.then((store) => {
|
|
535
|
+
if (!store)
|
|
536
|
+
return;
|
|
537
|
+
stopBackfill = startSessionBackfill({
|
|
538
|
+
log: (message) => {
|
|
539
|
+
// 日志同时走宿主 logger:appendLumeLog 依赖 DSH_HOME,宿主进程里可能没有 →
|
|
540
|
+
// 补蒸馏会“跑了却没有任何日志”(现场踩过:重启后零沉淀零日志)。
|
|
541
|
+
appendLumeLog(message);
|
|
542
|
+
ctx.logger?.warn?.(`lume: ${message}`);
|
|
543
|
+
},
|
|
544
|
+
addFact: (key, fact) => store.addFact(key, fact, isDuplicateFact),
|
|
545
|
+
});
|
|
546
|
+
})
|
|
547
|
+
.catch((error) => ctx.logger?.warn?.(`lume: 会话补蒸馏启动失败:${describeError(error)}`));
|
|
548
|
+
return () => stopBackfill?.();
|
|
549
|
+
});
|
|
550
|
+
const sessionEventDeps = assembleSessionEventDeps(wiring);
|
|
551
|
+
const toolDeps = assembleToolDeps(wiring);
|
|
552
|
+
const blockDeps = assembleBlockDeps(wiring);
|
|
553
|
+
/**
|
|
554
|
+
* 每轮落两条度量:一条**状态快照**(效能判定的基线:契约/设计/台账/假设/计数器)
|
|
555
|
+
* 与一条**块装配**(预算压力 + 本轮加权了哪三条条款)。
|
|
556
|
+
*
|
|
557
|
+
* 同一步里提示词会被构建多次,所以状态快照按**步**去重(步键 = 轮号 + 本轮工具调用数,见下)——
|
|
558
|
+
* 否则长轮次里只有轮首那一条,效能判定就没有分辨率(真机实测:跑了十几分钟只有 3 条)。
|
|
559
|
+
* 块装配不按步去重:它记的是「这一步实际注入了什么」,逐步都要。
|
|
560
|
+
*/
|
|
561
|
+
const metricsTurnSeen = new Map();
|
|
562
|
+
function recordTurnMetrics(sid, st, query, mode, composed) {
|
|
563
|
+
if (!metricsLog.enabled)
|
|
564
|
+
return;
|
|
565
|
+
const changes = changesOf(sid);
|
|
566
|
+
const unverified = changes.filter((item) => item.status !== "verified" && item.status !== "skipped").length;
|
|
567
|
+
const health = metricsLog.health(sid);
|
|
568
|
+
const state = {
|
|
569
|
+
hasContract: contractOf(sid) !== null,
|
|
570
|
+
unverifiedChanges: unverified,
|
|
571
|
+
// 纠正闭环:本模式被纠正过的次数与最后一次的轮号(与装配侧同一份数据,见 host/clauses.ts)
|
|
572
|
+
correctionModes: health.correctionsByMode,
|
|
573
|
+
lastCorrectionTurnByMode: health.lastCorrectionTurnByMode,
|
|
574
|
+
};
|
|
575
|
+
metricsLog.record({
|
|
576
|
+
kind: "blocks",
|
|
577
|
+
at: Date.now(),
|
|
578
|
+
sid,
|
|
579
|
+
turn: st.turnIndex,
|
|
580
|
+
mode,
|
|
581
|
+
kept: composed.kept,
|
|
582
|
+
dropped: composed.dropped,
|
|
583
|
+
chars: composed.chars,
|
|
584
|
+
budget: composed.budget,
|
|
585
|
+
focus: focusIdsFor(st, mode, query, state),
|
|
586
|
+
});
|
|
587
|
+
// 按**步**(而不是按轮)去重:一条长轮次里如果只记轮首快照,效能判定就没有分辨率
|
|
588
|
+
// (真机实测:一个跑了十几分钟的轮次只有 3 条记录)。步键取「轮号 + 本轮工具调用数」——
|
|
589
|
+
// 有工具动作才可能变,纯生成步骤不会灌水。
|
|
590
|
+
const stepKey = `${st.turnIndex}:${st.toolCalls}`;
|
|
591
|
+
if (metricsTurnSeen.get(sid) === stepKey)
|
|
592
|
+
return;
|
|
593
|
+
if (metricsTurnSeen.size > 400)
|
|
594
|
+
metricsTurnSeen.clear();
|
|
595
|
+
metricsTurnSeen.set(sid, stepKey);
|
|
596
|
+
metricsLog.record({
|
|
597
|
+
kind: "state",
|
|
598
|
+
at: Date.now(),
|
|
599
|
+
sid,
|
|
600
|
+
turn: st.turnIndex,
|
|
601
|
+
counters: { ...st.triggerCounters },
|
|
602
|
+
hasContract: state.hasContract,
|
|
603
|
+
designs: designOf(sid).length,
|
|
604
|
+
changes: changes.length,
|
|
605
|
+
unverified,
|
|
606
|
+
verified: changes.filter((item) => item.status === "verified").length,
|
|
607
|
+
hypotheses: hypothesesOf(sid).length,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
const sessionDisposedHandler = createSessionDisposedHandler(sessionEventDeps);
|
|
611
|
+
const sessionEventHandler = createSessionEventHandler(sessionEventDeps);
|
|
612
|
+
/** 工具注册的依赖(工具定义本体在 host/tools.ts)。 */
|
|
613
|
+
// 工具定义本体在 host/tools.ts(调用点必须在 deps 声明之后)
|
|
614
|
+
registerLumeTools(toolDeps);
|
|
1079
615
|
function computeTurn(sid, context) {
|
|
1080
616
|
const st = runtime.get(sid);
|
|
1081
617
|
// 意图取自会话权威历史(resolveIntent),不再读事件缓存里的最新文本:
|
|
1082
618
|
// 后者会被注入消息覆盖,且首步可能还带着上一轮的值,导致模式在轮内漂移。
|
|
1083
|
-
const { text: query, mode } = resolveIntent(context, st);
|
|
619
|
+
const { text: query, mode } = resolveIntent(sid, context, st);
|
|
620
|
+
// 路由自校:本会话已被用户纠正两次以上 → 顶一句带具体数字的提醒(上限 2 次防噪音)。
|
|
621
|
+
// 这是度量**回灌**到行为的那一环:只有记录没有回流,度量就只是日志。
|
|
622
|
+
const routeHealth = metricsLog.health(sid);
|
|
623
|
+
if (routeHealth.corrections >= 2 && noticeOpen(st, "metrics")) {
|
|
624
|
+
const modes = Object.entries(routeHealth.correctionsByMode)
|
|
625
|
+
.map(([name, count]) => `${name} ${count}`)
|
|
626
|
+
.join(" · ");
|
|
627
|
+
setNotice(st, "metrics", `〔路由自校〕本会话已有 ${routeHealth.corrections} 次判定被用户纠正${modes ? `(落在:${modes})` : ""}。这一轮先复述你理解的真实请求再动手;判不准就问一句,不要按上一轮的模式惯性继续。`);
|
|
628
|
+
}
|
|
1084
629
|
// 协议正文按模型能力冻结(不随 query 切变体):切变体会让系统提示词每轮改写,
|
|
1085
630
|
// 代价远超省下的几百 token。见 selectStableThinkingProtocol。
|
|
1086
631
|
const thinkingStable = selectStableThinkingProtocol({
|
|
1087
|
-
isReasoningModel: typeof llmRoute?.model === "string" && REASONING_MODEL_RE.test(llmRoute.model),
|
|
632
|
+
isReasoningModel: typeof llmRoute.current?.model === "string" && REASONING_MODEL_RE.test(llmRoute.current.model),
|
|
1088
633
|
}).trim();
|
|
1089
634
|
// 易变的任务指令:路由、阶段、闲聊声明、长会话护栏、目标锚点、即时对齐、
|
|
1090
635
|
// 交付复核、压缩重锚、文档能力指引、失败纠偏、反思提醒——全部每步可变。
|
|
1091
636
|
// 载具与方法块(契约/台账/假设/项目知识/影响面/文档方法/触发器提醒)排在最后:
|
|
1092
637
|
// 它们是「此刻最该看的」,紧贴尾部注意力最强位;超预算时先丢可丢块(composeBlocks)。
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
{ text: st.compaction ? buildCompactionNotice(st.compaction, st.turnIndex) : null },
|
|
1102
|
-
{ text: st.protocolCorrection },
|
|
1103
|
-
{ text: reflectionStore?.getFeedback() ?? null, droppable: true },
|
|
1104
|
-
...carrierBlocks(sid, context, st, query, mode),
|
|
1105
|
-
]);
|
|
638
|
+
// 记录工作目录:工具 exec / 会话事件里可能拿不到 cwd,项目键靠这里缓存兜住(实测项目知识曾落到 unknown)
|
|
639
|
+
if (typeof context?.agent?.session?.cwd === "string" && context.agent.session.cwd)
|
|
640
|
+
st.cwd = context.agent.session.cwd;
|
|
641
|
+
// cwd 到手就补落盘暂存的项目知识(这条路径是"cwd 后到"的主要补写时机)
|
|
642
|
+
flushPendingFacts(sid, context);
|
|
643
|
+
const composed = composeBlocksDetailed(volatileBlocks(blockDeps, { sid, context, st, query, mode }));
|
|
644
|
+
recordTurnMetrics(sid, st, query, mode, composed);
|
|
645
|
+
const thinkingRuntime = composed.text;
|
|
1106
646
|
// 会话选择尚未就绪(启动竞态):只出任务协议,人设段留空——与旧实现一致,
|
|
1107
647
|
// 也避免把「尚未选择」误记成一次人设切换。
|
|
1108
|
-
if (!currentStore)
|
|
648
|
+
if (!stores.currentStore())
|
|
1109
649
|
return { thinkingStable, thinkingRuntime, personaStable: "", personaData: "", boundary: "" };
|
|
1110
|
-
const selected = currentStore.get(sid);
|
|
650
|
+
const selected = stores.currentStore().get(sid);
|
|
1111
651
|
const personaName = selected ?? defaultName;
|
|
1112
652
|
const previous = st.lastInjected;
|
|
1113
653
|
if (previous !== undefined && previous !== personaName) {
|
|
@@ -1133,9 +673,9 @@ function applyInner(ctx, config = {}) {
|
|
|
1133
673
|
const personaStable = buildPersonaContractSection({ persona, profileName });
|
|
1134
674
|
const personaData = buildPersonaRuntimeSection({
|
|
1135
675
|
persona,
|
|
1136
|
-
memories: personaName ? identity?.getMemory(personaName) ?? [] : [],
|
|
1137
|
-
styleRules: personaName ? identity?.getStyleRules(personaName) ?? [] : [],
|
|
1138
|
-
corpusPins: personaName ? identity?.getCorpusPins(personaName) ?? [] : [],
|
|
676
|
+
memories: personaName ? (stores.identity()?.getMemory(personaName) ?? []) : [],
|
|
677
|
+
styleRules: personaName ? (stores.identity()?.getStyleRules(personaName) ?? []) : [],
|
|
678
|
+
corpusPins: personaName ? (stores.identity()?.getCorpusPins(personaName) ?? []) : [],
|
|
1139
679
|
query,
|
|
1140
680
|
turnIndex: st.turnIndex,
|
|
1141
681
|
sessionKey: sid,
|
|
@@ -1161,9 +701,7 @@ function applyInner(ctx, config = {}) {
|
|
|
1161
701
|
if (layeredOn)
|
|
1162
702
|
return part === "thinking" ? turn.thinkingStable : turn.personaStable;
|
|
1163
703
|
const stable = part === "thinking" ? turn.thinkingStable : turn.personaStable;
|
|
1164
|
-
const dynamic = part === "thinking"
|
|
1165
|
-
? turn.thinkingRuntime
|
|
1166
|
-
: [turn.personaData, turn.boundary].filter(Boolean).join("\n\n");
|
|
704
|
+
const dynamic = part === "thinking" ? turn.thinkingRuntime : [turn.personaData, turn.boundary].filter(Boolean).join("\n\n");
|
|
1167
705
|
return [stable, dynamic].filter(Boolean).join("\n\n");
|
|
1168
706
|
}
|
|
1169
707
|
/** 易变段取文:任务指令 / 人设数据 / 切换播报三段各自成 context,由宿主按 order 归并成一条尾部快照。 */
|
|
@@ -1179,38 +717,40 @@ function applyInner(ctx, config = {}) {
|
|
|
1179
717
|
return builtins;
|
|
1180
718
|
},
|
|
1181
719
|
get store() {
|
|
1182
|
-
return currentStore;
|
|
720
|
+
return stores.currentStore();
|
|
1183
721
|
},
|
|
1184
722
|
get registry() {
|
|
1185
723
|
return registry;
|
|
1186
724
|
},
|
|
1187
725
|
get identity() {
|
|
1188
|
-
return identity;
|
|
726
|
+
return stores.identity();
|
|
1189
727
|
},
|
|
1190
728
|
get distill() {
|
|
1191
729
|
return distillRunner;
|
|
1192
730
|
},
|
|
1193
731
|
getProjectState(sessionId) {
|
|
1194
732
|
// 诊断视图:任务载具 + 项目知识(供排查"模型到底看到了什么")。
|
|
1195
|
-
if (!project)
|
|
733
|
+
if (!stores.project())
|
|
1196
734
|
return null;
|
|
1197
735
|
const st = runtime.get(sessionId);
|
|
1198
736
|
return {
|
|
1199
737
|
projectKey: st.projectKey,
|
|
1200
|
-
contract: project.getContract(sessionId),
|
|
1201
|
-
changes: project.getChanges(sessionId),
|
|
1202
|
-
hypotheses: project.getHypotheses(sessionId),
|
|
1203
|
-
facts: st.projectKey ? project.getFacts(st.projectKey) : [],
|
|
738
|
+
contract: stores.project().getContract(sessionId),
|
|
739
|
+
changes: stores.project().getChanges(sessionId),
|
|
740
|
+
hypotheses: stores.project().getHypotheses(sessionId),
|
|
741
|
+
facts: st.projectKey ? stores.project().getFacts(st.projectKey) : [],
|
|
742
|
+
design: stores.project() ? stores.project().getDesign(sessionId) : [],
|
|
743
|
+
requirements: stores.project() ? stores.project().getRequirements(sessionId) : [],
|
|
1204
744
|
triggers: { ...st.triggerCounters, fired: st.triggerFiredAt },
|
|
1205
745
|
};
|
|
1206
746
|
},
|
|
1207
747
|
async clearProjectFacts(sessionId) {
|
|
1208
|
-
if (!project)
|
|
748
|
+
if (!stores.project())
|
|
1209
749
|
return false;
|
|
1210
750
|
const st = runtime.get(sessionId);
|
|
1211
751
|
if (!st.projectKey)
|
|
1212
752
|
return false;
|
|
1213
|
-
await project.clearFacts(st.projectKey);
|
|
753
|
+
await stores.project().clearFacts(st.projectKey);
|
|
1214
754
|
return true;
|
|
1215
755
|
},
|
|
1216
756
|
});
|
|
@@ -1218,66 +758,25 @@ function applyInner(ctx, config = {}) {
|
|
|
1218
758
|
// 挪到 apply 的最后注册:它只服务客户端菜单(人设列表/蒸馏/管理),
|
|
1219
759
|
// 绝不该挡住人设段、工具与易变段的注册(0.7.1 的教训:这里抛错 → 整段 apply 中断 → 界面看不到人设)。
|
|
1220
760
|
// 注册本身见下方 registerRpcChannel()。
|
|
1221
|
-
// ──
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
// 易变段走 runtime-context 通道:宿主把它渲染成对话尾部的一条快照消息(文案自带
|
|
1231
|
-
// "supersedes earlier runtime-context snapshots",新快照取代旧快照,不堆叠进历史),
|
|
1232
|
-
// 因此它的每一次变化只花自己那几百 token,不作废前面的任何前缀。
|
|
1233
|
-
// 宿主不支持该 API 时 layeredOn=false,易变段已并回 system 段(见 systemSectionText)。
|
|
1234
|
-
if (layeredOn) {
|
|
1235
|
-
const dynamicContexts = [
|
|
761
|
+
// ── 系统提示词段与易变段注册(三条通道的用意见 host/sections.ts)──
|
|
762
|
+
installPromptSections({
|
|
763
|
+
ctx,
|
|
764
|
+
layeredOn,
|
|
765
|
+
personaSection: LUME_PERSONA_SECTION,
|
|
766
|
+
personaOrder,
|
|
767
|
+
thinkingSection: LUME_THINKING_SECTION,
|
|
768
|
+
thinkingOrder: LUME_THINKING_ORDER,
|
|
769
|
+
contexts: [
|
|
1236
770
|
{ name: LUME_RUNTIME_CONTEXT, order: LUME_RUNTIME_ORDER, part: "thinking" },
|
|
1237
771
|
{ name: LUME_PERSONA_RUNTIME_CONTEXT, order: LUME_PERSONA_RUNTIME_ORDER, part: "persona" },
|
|
1238
772
|
{ name: LUME_BOUNDARY_CONTEXT, order: LUME_BOUNDARY_ORDER, part: "boundary" },
|
|
1239
|
-
]
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
return sid ? runtimeContextText(String(sid), context, entry.part) : "";
|
|
1247
|
-
},
|
|
1248
|
-
}), `lume.runtime-context(${entry.name})`);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
ctx.effect(() => ctx.systemPrompt.section({
|
|
1252
|
-
name: LUME_THINKING_SECTION,
|
|
1253
|
-
order: LUME_THINKING_ORDER,
|
|
1254
|
-
text: (context) => {
|
|
1255
|
-
const sid = context.agent?.session?.id ?? context.agent?.id;
|
|
1256
|
-
return sid ? systemSectionText(String(sid), context, "thinking") : "";
|
|
1257
|
-
},
|
|
1258
|
-
}), "lume.thinking-section()");
|
|
1259
|
-
// 工具失败提示走 runtime-context 通道:宿主把它渲染成对话尾部的一条消息,
|
|
1260
|
-
// 而不是拼进 system 串。system 串只要变化就会写一条新的 request/header,
|
|
1261
|
-
// 既在界面上多出一行「系统提示词」,也让前缀缓存从系统提示词处整段失效。
|
|
1262
|
-
// `systemPrompt.context` 是宿主较新版本才有的 API,缺失时静默跳过(旧宿主下
|
|
1263
|
-
// 只是失去这条提示,不影响其余功能)。
|
|
1264
|
-
ctx.effect(() => {
|
|
1265
|
-
if (typeof ctx.systemPrompt?.context !== "function") {
|
|
1266
|
-
ctx.logger?.warn?.("lume: 当前宿主不支持 systemPrompt.context,工具失败提示已跳过(不影响其余功能)");
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
return ctx.systemPrompt.context({
|
|
1270
|
-
name: LUME_TOOL_NOTICE_CONTEXT,
|
|
1271
|
-
order: LUME_TOOL_NOTICE_ORDER,
|
|
1272
|
-
text: (context) => {
|
|
1273
|
-
const sid = context.agent?.session?.id ?? context.agent?.id;
|
|
1274
|
-
const st = sid ? runtime.get(String(sid)) : null;
|
|
1275
|
-
if (!st)
|
|
1276
|
-
return "";
|
|
1277
|
-
return buildToolFailureNotice({ failures: st.toolFailures, unknown: st.toolUnknown }) ?? "";
|
|
1278
|
-
},
|
|
1279
|
-
});
|
|
1280
|
-
}, "lume.tool-notice-context()");
|
|
773
|
+
],
|
|
774
|
+
systemSectionText,
|
|
775
|
+
runtimeContextText,
|
|
776
|
+
toolNoticeContext: { name: LUME_TOOL_NOTICE_CONTEXT, order: LUME_TOOL_NOTICE_ORDER },
|
|
777
|
+
runtime,
|
|
778
|
+
buildToolFailureNotice,
|
|
779
|
+
});
|
|
1281
780
|
registerRpcChannel(ctx);
|
|
1282
781
|
/**
|
|
1283
782
|
* 注册客户端 RPC 通道(`/lume`)。放在最后 + 独立 try/catch:
|
|
@@ -1292,8 +791,7 @@ function applyInner(ctx, config = {}) {
|
|
|
1292
791
|
*/
|
|
1293
792
|
function registerRpcChannel(scope) {
|
|
1294
793
|
const dispatch = async (endpoint, payload) => {
|
|
1295
|
-
|
|
1296
|
-
identity ??= await identityReady;
|
|
794
|
+
await stores.ensureReady();
|
|
1297
795
|
return handleEndpoint(endpoint, payload);
|
|
1298
796
|
};
|
|
1299
797
|
// 依赖组合与宿主自带 dsh-api-gateway 一致(["connection", "webServer"])。
|