lume-dsh-plugin 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +81 -20
- package/assets/personalities/butler-corpus.jsonl +0 -0
- package/assets/personalities/butler.txt +0 -0
- package/assets/personalities/loli-corpus.jsonl +30 -30
- package/assets/personalities/loli.txt +12 -12
- package/assets/personalities/none-corpus.jsonl +0 -0
- package/assets/personalities/none.txt +0 -0
- package/assets/personalities/senpai-corpus.jsonl +30 -30
- package/assets/personalities/senpai.txt +12 -12
- package/assets/personalities/tsundere-corpus.jsonl +0 -0
- package/assets/personalities/tsundere.txt +0 -0
- package/assets/personalities.json +0 -0
- package/cordis.patch.yml +0 -0
- package/lib/client.js +0 -0
- package/lib/core/card.js +0 -0
- package/lib/core/dialogue-mining.js +0 -0
- package/lib/core/leak-detector.js +0 -0
- package/lib/core/ledger.js +211 -0
- package/lib/core/manifest.js +0 -0
- package/lib/core/persona-text.js +0 -0
- package/lib/core/retrieval.js +0 -0
- package/lib/core/sampling.js +0 -0
- package/lib/core/signals.js +64 -0
- package/lib/core/text.js +0 -0
- package/lib/host/boundary.js +0 -0
- package/lib/host/compaction.js +0 -0
- package/lib/host/diag.js +0 -0
- package/lib/host/distill.js +0 -0
- package/lib/host/documents.js +0 -0
- package/lib/host/extraction.js +0 -0
- package/lib/host/identity.js +0 -0
- package/lib/host/injection.js +73 -26
- package/lib/host/methods.js +64 -0
- package/lib/host/personalities.js +0 -0
- package/lib/host/project.js +222 -0
- package/lib/host/protocol.js +12 -0
- package/lib/host/reflection.js +27 -6
- package/lib/host/registry.js +0 -0
- package/lib/host/rpc.js +15 -0
- package/lib/host/session-runtime.js +11 -2
- package/lib/host/store.js +0 -0
- package/lib/host/thinking.js +19 -0
- package/lib/host/triggers.js +133 -0
- package/lib/index.js +488 -109
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
|
18
18
|
import { BlockAssembler, createUserMessage, ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
19
19
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
20
20
|
import z from "@deepseek-ai/schemastery";
|
|
21
|
-
import {
|
|
21
|
+
import { buildPersonaContractSection, buildPersonaRuntimeSection } from "./host/injection.js";
|
|
22
22
|
import { loadPersonalities, NONE_PERSONA } from "./host/personalities.js";
|
|
23
23
|
import { createLumeRpcHandler } from "./host/rpc.js";
|
|
24
24
|
import { FilePersonaStore, migrateLegacyState, PersonaStore } from "./host/store.js";
|
|
@@ -27,6 +27,7 @@ import { PersonaRegistry } from "./host/registry.js";
|
|
|
27
27
|
import { buildCorrectionPrompt, buildExtractionPrompt, extractNaming, isCoolingDown, isDuplicateFact, mergeNewFacts, parseCorrectionRule, parseFacts, resolveAuxRoute, shouldCaptureCorpus, shouldConsider, shouldConsiderCorrection } from "./host/extraction.js";
|
|
28
28
|
import { DistillJobRunner, DISTILL_ALGORITHM_VERSION, runDistill } from "./host/distill.js";
|
|
29
29
|
import { jaccard } from "./core/retrieval.js";
|
|
30
|
+
import { fnv1a32 } from "./core/sampling.js";
|
|
30
31
|
import { detectLeak } from "./core/leak-detector.js";
|
|
31
32
|
import { messageText } from "./core/text.js";
|
|
32
33
|
import { composeBoundary } from "./host/boundary.js";
|
|
@@ -34,9 +35,14 @@ import { SessionRuntimeStore } from "./host/session-runtime.js";
|
|
|
34
35
|
import { isCompactionCheckpoint } from "./host/compaction.js";
|
|
35
36
|
import { LUME_REFLECTION_SPEC, ReflectionStore, buildReflectionPrompt, parseReflectionScore } from "./host/reflection.js";
|
|
36
37
|
import { appendLumeLog } from "./host/diag.js";
|
|
37
|
-
import { advancePhase, buildAlignmentCorrection, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyInteraction, isUserAuthored, taskPhaseForMode } from "./host/protocol.js";
|
|
38
|
+
import { advancePhase, buildAlignmentCorrection, buildCasualDirective, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyInteraction, isUserAuthored, taskPhaseForMode } from "./host/protocol.js";
|
|
38
39
|
import { buildDocumentDirective, probeDocumentCapabilities } from "./host/documents.js";
|
|
39
|
-
import { REASONING_MODEL_RE, TASK_SIGNAL_RE,
|
|
40
|
+
import { REASONING_MODEL_RE, TASK_SIGNAL_RE, selectStableThinkingProtocol } from "./host/thinking.js";
|
|
41
|
+
import { normalizeChange, normalizeContract, normalizeHypothesis, normalizeProjectFact, projectKeyOf, renderChangeLedger, renderContract, renderHypotheses, renderProjectFacts } from "./core/ledger.js";
|
|
42
|
+
import { classifyTool, readResultSignals } from "./core/signals.js";
|
|
43
|
+
import { buildContractMethodDirective, buildDocumentMethodDirective, buildImpactDirective, buildStructureHint, composeBlocks } from "./host/methods.js";
|
|
44
|
+
import { LUME_PROJECT_SPEC, ProjectStore } from "./host/project.js";
|
|
45
|
+
import { DEFAULT_TRIGGER_THRESHOLDS, applyToolSignal, applyVerifyOutcome, cooldownOk, evaluateToolTrigger, evaluateTurnTrigger } from "./host/triggers.js";
|
|
40
46
|
/** schemastery → domainTable 形参的桥接(与 identity.ts 同款)。 */
|
|
41
47
|
const recordSchema = zodLike;
|
|
42
48
|
/** 会话人设选择的持久层(键 = sessionId)。 */
|
|
@@ -56,16 +62,38 @@ const LLM_DUMP_PATH = process.env.DSH_HOME
|
|
|
56
62
|
const LUME_PERSONA_SECTION = "lume:persona";
|
|
57
63
|
const LUME_THINKING_SECTION = "lume:thinking";
|
|
58
64
|
const LUME_THINKING_ORDER = 1;
|
|
59
|
-
/**
|
|
60
|
-
* 宿主的段落布局是:身份声明 -1000(最前,"你是 AI 助手"
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* "AI 助手"自居。放在最末尾(紧贴对话历史、注意力最强)后,人格合规才成立。
|
|
65
|
+
/** 人设契约段(会话恒定)的 order:取 10000——真正的 system prompt 末尾。
|
|
66
|
+
* 宿主的段落布局是:身份声明 -1000(最前,"你是 AI 助手"的来源)、部署 persona 前缀 0、
|
|
67
|
+
* 策略 500-900、工具定义 1000-5000、结构化输出 9900。人设若按惯例放 order 2,会被压在
|
|
68
|
+
* 头部身份声明与近万 token 工具内容之间——实测模型会无视中段的人设契约、直接以
|
|
69
|
+
* "AI 助手"自居。放在最末尾(紧贴对话历史、注意力最强)后,人格合规才成立。
|
|
70
|
+
* 该段在一个会话内必须逐字节恒定:见 LUME_RUNTIME_CONTEXT 的说明。 */
|
|
64
71
|
const LUME_PERSONA_ORDER = 10000;
|
|
65
|
-
|
|
72
|
+
/**
|
|
73
|
+
* 易变注入段(记忆 top-k、风格约定、语料示例、切换播报、路由/阶段/护栏/锚点、
|
|
74
|
+
* 压缩重锚、文档指引)——走宿主 runtime-context 通道,渲染成对话尾部的一条快照消息。
|
|
75
|
+
*
|
|
76
|
+
* 为什么不能留在 system 段:system 串排在消息序列最前面,而前缀缓存只认「从第一个
|
|
77
|
+
* 不同的字节起,之后全部失效」。只要 system 串每步变一次,它后面的工具定义、结构化
|
|
78
|
+
* 输出和**整段对话历史**就全部按全价重算。实测(deepseek-v4-flash,2026-09-11 某会话
|
|
79
|
+
* 282 个请求):cacheRead 恒定 384 token、命中率中位数 0.2%;当日全站命中率 18%。
|
|
80
|
+
* 而宿主内置的 `systemPromptUpdate: "in-history"`(把系统提示词变化以追加方式落在
|
|
81
|
+
* 历史尾部)只在 `deepseek-flash` 一个内置模型条目上声明,v4 系没有——所以这层拆分
|
|
82
|
+
* 是插件侧唯一能拿回缓存的手段。
|
|
83
|
+
*
|
|
84
|
+
* 宿主的 runtime-context 语义正好对症:assemble 时求值、append 到消息尾部,且文案声明
|
|
85
|
+
* "supersedes earlier runtime-context snapshots"(新快照取代旧快照,不堆叠进历史),
|
|
86
|
+
* 因此它每一次变化只花自己那几百 token。
|
|
87
|
+
*/
|
|
88
|
+
const LUME_RUNTIME_CONTEXT = "lume:runtime";
|
|
89
|
+
const LUME_RUNTIME_ORDER = 10000;
|
|
90
|
+
/** 人设易变段(记忆 top-k / 风格约定 / 语料示例):同走 runtime-context,排在任务指令之后。 */
|
|
91
|
+
const LUME_PERSONA_RUNTIME_CONTEXT = "lume:persona-runtime";
|
|
92
|
+
const LUME_PERSONA_RUNTIME_ORDER = 10050;
|
|
93
|
+
/** 人设切换播报:独立成段(只在切换窗口内有内容),便于单独观测与测试。 */
|
|
94
|
+
const LUME_BOUNDARY_CONTEXT = "lume:boundary";
|
|
66
95
|
const LUME_BOUNDARY_ORDER = 10100;
|
|
67
|
-
/**
|
|
68
|
-
* 不进 system 串——避免会变的内容每步作废前缀缓存。 */
|
|
96
|
+
/** 工具失败提示:同走 runtime-context 通道,排在易变段之后。 */
|
|
69
97
|
const LUME_TOOL_NOTICE_CONTEXT = "lume:tool-notice";
|
|
70
98
|
const LUME_TOOL_NOTICE_ORDER = 10150;
|
|
71
99
|
const MAX_SESSIONS = 200;
|
|
@@ -90,6 +118,14 @@ export function apply(ctx, config = {}) {
|
|
|
90
118
|
const distillRouteOverride = { provider: config.distillProvider, model: config.distillModel };
|
|
91
119
|
const reflectionEnabled = config.reflectionEnabled ?? true;
|
|
92
120
|
const boundaryTurns = config.switchBoundaryTurns ?? SWITCH_BOUNDARY_TURNS;
|
|
121
|
+
const layeredInjection = config.layeredInjection ?? true;
|
|
122
|
+
/** 宿主是否提供 runtime-context 通道(0.1.5 起的 API)。 */
|
|
123
|
+
const runtimeContextSupported = typeof ctx.systemPrompt?.context === "function";
|
|
124
|
+
/** 是否真的走分层:用户没关掉、且宿主支持。否则易变段并回 system 段(旧行为)。 */
|
|
125
|
+
const layeredOn = layeredInjection && runtimeContextSupported;
|
|
126
|
+
if (layeredInjection && !runtimeContextSupported) {
|
|
127
|
+
ctx.logger?.warn?.("lume: 当前宿主不支持 systemPrompt.context,易变注入段并回系统提示词(前缀缓存收益消失,功能不受影响)");
|
|
128
|
+
}
|
|
93
129
|
const defaultName = builtins[NONE_PERSONA] ? NONE_PERSONA : null;
|
|
94
130
|
const legacyStatePath = join(assetsDir, "persona-state.json");
|
|
95
131
|
// ── 存储就绪:会话选择域(必有)+ 身份域(失败降级为无档案功能)──
|
|
@@ -155,9 +191,38 @@ export function apply(ctx, config = {}) {
|
|
|
155
191
|
}
|
|
156
192
|
})();
|
|
157
193
|
void reflectionReady.then((s) => { reflectionStore = s; });
|
|
194
|
+
// ── 项目域:任务契约 / 改动台账 / 假设台账 / 项目知识(失败降级为无载具功能)──
|
|
195
|
+
const projectMemoryOn = config.projectMemory ?? true;
|
|
196
|
+
const behaviorTriggersOn = config.behaviorTriggers ?? true;
|
|
197
|
+
const triggerThresholds = {
|
|
198
|
+
...DEFAULT_TRIGGER_THRESHOLDS,
|
|
199
|
+
inspectStreak: config.triggerInspectStreak ?? DEFAULT_TRIGGER_THRESHOLDS.inspectStreak,
|
|
200
|
+
changeStreak: config.triggerChangeStreak ?? DEFAULT_TRIGGER_THRESHOLDS.changeStreak,
|
|
201
|
+
deadPathFails: config.triggerDeadPathFails ?? DEFAULT_TRIGGER_THRESHOLDS.deadPathFails,
|
|
202
|
+
};
|
|
203
|
+
let project = null;
|
|
204
|
+
const projectReady = (async () => {
|
|
205
|
+
if (!projectMemoryOn)
|
|
206
|
+
return null;
|
|
207
|
+
try {
|
|
208
|
+
const domain = await ctx.storageDomain.open(LUME_PROJECT_SPEC);
|
|
209
|
+
ctx.effect(() => async () => { await domain.close(); }, "lume: close project domain");
|
|
210
|
+
return new ProjectStore({
|
|
211
|
+
contract: domain.table("contract"),
|
|
212
|
+
ledger: domain.table("ledger"),
|
|
213
|
+
hypotheses: domain.table("hypotheses"),
|
|
214
|
+
facts: domain.table("facts"),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
ctx.logger?.warn?.("lume: 项目域不可用,任务契约/台账/项目知识降级", error);
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
})();
|
|
222
|
+
void projectReady.then((s) => { project = s; });
|
|
158
223
|
const registry = new PersonaRegistry(builtins, () => identity);
|
|
159
224
|
ctx.logger?.warn?.(`lume: 已加载(builtins=${Object.keys(builtins).join(",") || "空!"},assets=${assetsDir})`);
|
|
160
|
-
ctx.logger?.warn?.(`lume:
|
|
225
|
+
ctx.logger?.warn?.(`lume: llmRoute 初始化策略:agentDefaultModel → settings → 回退`);
|
|
161
226
|
// ── 每会话运行时状态(内存,重启即弃,LRU 上限兜底)──
|
|
162
227
|
const runtime = new SessionRuntimeStore();
|
|
163
228
|
// ── 模型路由缓存(request/context,会话过程中由 agent-loop 更新)──
|
|
@@ -480,6 +545,9 @@ export function apply(ctx, config = {}) {
|
|
|
480
545
|
st.toolCalls++;
|
|
481
546
|
if (st.interactionMode === "execute")
|
|
482
547
|
st.taskPhase = advancePhase(st.taskPhase, "execute");
|
|
548
|
+
// 行为类别在调用阶段记账(连击),成败到结果阶段才结算。
|
|
549
|
+
st.toolKind = classifyTool(event.data?.name);
|
|
550
|
+
applyToolSignal(st.triggerCounters, st.toolKind, null);
|
|
483
551
|
break;
|
|
484
552
|
}
|
|
485
553
|
case "tool/result": {
|
|
@@ -495,6 +563,30 @@ export function apply(ctx, config = {}) {
|
|
|
495
563
|
st.toolSuccesses++;
|
|
496
564
|
if (st.interactionMode === "execute")
|
|
497
565
|
st.taskPhase = advancePhase(st.taskPhase, unknownResult || explicitError ? "diagnose" : "verify");
|
|
566
|
+
// 行为信号 → 计数器 → 触发器提醒。提醒只在这一步之后可见(尾部快照),
|
|
567
|
+
// 且每类触发器有轮级冷却:提示一多就变噪音,模型会学会忽略。
|
|
568
|
+
const signals = readResultSignals(resultText, explicitError);
|
|
569
|
+
applyVerifyOutcome(st.triggerCounters, st.toolKind, signals);
|
|
570
|
+
if (behaviorTriggersOn) {
|
|
571
|
+
const fire = evaluateToolTrigger(st.triggerCounters, {
|
|
572
|
+
turnIndex: st.turnIndex,
|
|
573
|
+
isTask: isTaskQuery(st),
|
|
574
|
+
diagnosing: st.interactionMode === "diagnosis",
|
|
575
|
+
hasContract: contractOf(sid) !== null,
|
|
576
|
+
hasHypotheses: hypothesesOf(sid).length > 0,
|
|
577
|
+
hypothesesTouched: st.hypothesesTouched,
|
|
578
|
+
}, triggerThresholds);
|
|
579
|
+
if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
580
|
+
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
581
|
+
st.triggerNudge = fire.text;
|
|
582
|
+
ctx.logger?.warn?.(`lume: [${sid}] 行为触发器 ${fire.id}(steps=${st.triggerCounters.steps},inspect=${st.triggerCounters.inspectStreak},mutate=${st.triggerCounters.mutateStreak},verifyFail=${st.triggerCounters.verifyFailStreak})`);
|
|
583
|
+
// 环境性死路自动落成项目知识:下次会话不必重踩。
|
|
584
|
+
if (fire.id === "dead-path" && signals.env && project) {
|
|
585
|
+
const key = projectKeyFor(sid, session);
|
|
586
|
+
void project.addFact(key, normalizeProjectFact({ kind: "deadend", text: `本环境验证受阻(${st.triggerCounters.verifyFailStreak} 次连续失败,环境/依赖类):换降级阶梯,不要重复同一命令` }, Date.now()), (candidate, existing) => existing.some((fact) => fact.text === candidate));
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
498
590
|
break;
|
|
499
591
|
}
|
|
500
592
|
case "compaction/summary": {
|
|
@@ -509,6 +601,43 @@ export function apply(ctx, config = {}) {
|
|
|
509
601
|
}
|
|
510
602
|
case "turn/end": {
|
|
511
603
|
st.turnIndex++;
|
|
604
|
+
// 切换窗口的消耗只发生在轮边界(渲染函数只读状态,不再就地清零):
|
|
605
|
+
// 同一步里 prompt 会被构建多次,若在渲染里消耗窗口,第二次构建就会
|
|
606
|
+
// 丢掉接班招呼——那是「注入随构建次数漂移」,正是本版要消灭的东西。
|
|
607
|
+
st.switchGreetingPending = false;
|
|
608
|
+
if (st.switchTurn !== null && st.turnIndex - st.switchTurn >= boundaryTurns)
|
|
609
|
+
st.switchTurn = null;
|
|
610
|
+
// ── 行为触发器(轮边界)──
|
|
611
|
+
// 连击按轮清零:新一轮是新请求,上一轮的「撒网」不该继续累加;死路连击
|
|
612
|
+
// 跨轮保留(同一环境不可用是会话级事实)。上轮的提醒到这里失效。
|
|
613
|
+
st.triggerCounters.inspectStreak = 0;
|
|
614
|
+
st.triggerCounters.mutateStreak = 0;
|
|
615
|
+
st.triggerNudge = null;
|
|
616
|
+
st.turnNudge = null;
|
|
617
|
+
st.hypothesesTouched = false;
|
|
618
|
+
if (behaviorTriggersOn && project) {
|
|
619
|
+
const fire = evaluateTurnTrigger({
|
|
620
|
+
turnIndex: st.turnIndex,
|
|
621
|
+
hasContract: contractOf(sid) !== null,
|
|
622
|
+
compactionTurn: st.compaction?.turnIndex ?? null,
|
|
623
|
+
lastDriftTurn: st.lastDriftTurn,
|
|
624
|
+
counters: st.triggerCounters,
|
|
625
|
+
knowledgePrompted: st.knowledgePrompted,
|
|
626
|
+
}, triggerThresholds);
|
|
627
|
+
if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
628
|
+
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
629
|
+
if (fire.id === "criteria-drift") {
|
|
630
|
+
// 契约对账用「交付口径」渲染原始判据:防判据随进展漂移。
|
|
631
|
+
st.lastDriftTurn = st.turnIndex;
|
|
632
|
+
st.turnNudge = renderContract(contractOf(sid), true);
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
st.knowledgePrompted = true;
|
|
636
|
+
st.turnNudge = fire.text;
|
|
637
|
+
}
|
|
638
|
+
ctx.logger?.warn?.(`lume: [${sid}] 轮触发器 ${fire.id}(turn=${st.turnIndex})`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
512
641
|
// 低成本会话内纠偏:只处理明确的错误/失败信号,且要求连续轮次用户请求相同。
|
|
513
642
|
const failed = /失败|报错|错误|exception|traceback|cannot|unable|permission denied|timed out|找不到|不存在/i.test(st.assistantText);
|
|
514
643
|
const queryKey = st.userText.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
@@ -565,6 +694,8 @@ export function apply(ctx, config = {}) {
|
|
|
565
694
|
const st = runtime.get(sid);
|
|
566
695
|
const turns = [...st.recentTurns];
|
|
567
696
|
runtime.delete(sid);
|
|
697
|
+
// 任务载具是会话态:任务结束即无意义,清掉避免无界增长(项目知识在另一张表,不受影响)。
|
|
698
|
+
void projectReady.then((store) => store?.clearSession(sid));
|
|
568
699
|
// 反思日志:会话结束后空闲时间跑一次小模型,零用户感知 token。
|
|
569
700
|
// 历史不够长(< 4 条消息)或路由不可用时静默跳过。
|
|
570
701
|
if (reflectionEnabled && turns.length >= 4) {
|
|
@@ -583,10 +714,89 @@ export function apply(ctx, config = {}) {
|
|
|
583
714
|
if (!score)
|
|
584
715
|
return;
|
|
585
716
|
await store.log(sid, score);
|
|
586
|
-
ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review}「${score.note}」`);
|
|
717
|
+
ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review} diagnosis=${score.diagnosis}「${score.note}」`);
|
|
587
718
|
})();
|
|
588
719
|
}
|
|
589
720
|
}), "lume: session disposal");
|
|
721
|
+
// ── 载具与项目知识的读取入口(事件处理器 / 工具 / 注入三处共用)──
|
|
722
|
+
/** 项目键:优先取会话工作目录(跨会话共享同一仓库的知识)。 */
|
|
723
|
+
function projectKeyFor(sid, source) {
|
|
724
|
+
const st = runtime.get(sid);
|
|
725
|
+
if (st.projectKey)
|
|
726
|
+
return st.projectKey;
|
|
727
|
+
// 三种调用来源:提示词 context({agent:{session}})、工具 exec({agent:{session}})、
|
|
728
|
+
// 会话事件(session 本身)。统一取到 session 再读 cwd。
|
|
729
|
+
const session = source?.agent?.session ?? source?.session ?? source;
|
|
730
|
+
const cwd = session?.cwd ?? "";
|
|
731
|
+
const key = projectKeyOf(cwd);
|
|
732
|
+
// 只有拿到真实工作目录才缓存:否则一次无 cwd 的调用会把 "unknown" 固化下来。
|
|
733
|
+
if (cwd)
|
|
734
|
+
st.projectKey = key;
|
|
735
|
+
return key;
|
|
736
|
+
}
|
|
737
|
+
function isTaskQuery(st) {
|
|
738
|
+
return TASK_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? "");
|
|
739
|
+
}
|
|
740
|
+
function contractOf(sid) {
|
|
741
|
+
return project?.getContract(sid) ?? null;
|
|
742
|
+
}
|
|
743
|
+
function changesOf(sid) {
|
|
744
|
+
return project?.getChanges(sid) ?? [];
|
|
745
|
+
}
|
|
746
|
+
function hypothesesOf(sid) {
|
|
747
|
+
return project?.getHypotheses(sid) ?? [];
|
|
748
|
+
}
|
|
749
|
+
function factsOf(sid, context) {
|
|
750
|
+
return project ? project.getFacts(projectKeyFor(sid, context)) : [];
|
|
751
|
+
}
|
|
752
|
+
/** 环境里是否有符号级结构分析工具:有就让模型用它替代通篇 read。 */
|
|
753
|
+
function structureToolName(context) {
|
|
754
|
+
try {
|
|
755
|
+
const schemas = ctx.get("tools")?.schemas?.(context?.agent);
|
|
756
|
+
if (!Array.isArray(schemas))
|
|
757
|
+
return null;
|
|
758
|
+
for (const schema of schemas) {
|
|
759
|
+
const name = String(schema?.name ?? "");
|
|
760
|
+
if (/analy|tree|symbol|lsp|reference|code_map|outline/i.test(name))
|
|
761
|
+
return name;
|
|
762
|
+
}
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* 任务载具块:契约 / 改动台账 / 假设台账 / 项目知识 + 方法块 + 触发器提醒。
|
|
771
|
+
*
|
|
772
|
+
* 全部落在尾部快照(易变层)——这正是「载具」现在才做得起的原因:0.6.2 之前每步
|
|
773
|
+
* 注入一份会变的状态等于每步作废整段前缀,而快照只在内容变化时才付费(实测 58 步
|
|
774
|
+
* 只产生 9 条快照)。顺序上把「此刻最该做的一件事」(触发器提醒)放在最后。
|
|
775
|
+
*/
|
|
776
|
+
function carrierBlocks(sid, context, st, query, mode) {
|
|
777
|
+
if (!projectMemoryOn)
|
|
778
|
+
return [];
|
|
779
|
+
const isTask = TASK_SIGNAL_RE.test(query);
|
|
780
|
+
const contract = contractOf(sid);
|
|
781
|
+
const changes = changesOf(sid);
|
|
782
|
+
const docDirective = buildDocumentDirective({ query, capabilities: probeDocumentCapabilities(ctx.get("tools"), context?.agent) });
|
|
783
|
+
return [
|
|
784
|
+
// 契约:有就回显(交付轮切成对账口径),没有且是任务轮就先教它写一份。
|
|
785
|
+
{ text: renderContract(contract, st.taskPhase === "deliver") },
|
|
786
|
+
{ text: !contract && isTask ? buildContractMethodDirective() : null, droppable: true },
|
|
787
|
+
// 台账与假设:存在就回显——让模型「看见」自己的计划,而不是记在脑子里。
|
|
788
|
+
{ text: changes.length > 0 ? renderChangeLedger(changes) : null },
|
|
789
|
+
{ text: renderHypotheses(hypothesesOf(sid)) },
|
|
790
|
+
// 项目知识:只在与项目相关的轮次出现(闲聊不该背仓库事实)。
|
|
791
|
+
{ text: isTask ? renderProjectFacts(factsOf(sid, context)) : null, droppable: true },
|
|
792
|
+
// 方法块:按任务形态出现;文档方法论只在判定为文档任务时出现。
|
|
793
|
+
{ text: isTask && mode !== "question" ? buildImpactDirective() : null, droppable: true },
|
|
794
|
+
{ text: docDirective ? buildDocumentMethodDirective() : null },
|
|
795
|
+
{ text: mode === "execute" || mode === "diagnosis" ? buildStructureHint(structureToolName(context)) : null, droppable: true },
|
|
796
|
+
{ text: st.turnNudge },
|
|
797
|
+
{ text: st.triggerNudge },
|
|
798
|
+
];
|
|
799
|
+
}
|
|
590
800
|
// ── 模型可调用工具(主写入通道)──
|
|
591
801
|
// 工具 output schema 的 const 语义要求成功值恒为 { ok: true };失败一律抛错交由框架呈现。
|
|
592
802
|
// as const 让 defineTool 从字面量推断 O,三个工具共用同一份成功形状。
|
|
@@ -657,6 +867,152 @@ export function apply(ctx, config = {}) {
|
|
|
657
867
|
return { ok: true };
|
|
658
868
|
},
|
|
659
869
|
}));
|
|
870
|
+
// ── 任务载具工具(第二组写入通道)──
|
|
871
|
+
// 与人格工具一样是「模型主动调用、零额外 LLM 调用」,区别在写入对象:契约/台账/假设
|
|
872
|
+
// 属于当前任务(会话态),项目知识按工作目录跨会话累积。列表类参数统一用字符串
|
|
873
|
+
// 分隔(分号或换行),不引入数组 schema——省 schema token,也少一层校验风险。
|
|
874
|
+
const splitList = (value) => String(value ?? "")
|
|
875
|
+
.split(/[;;\n]/)
|
|
876
|
+
.map((item) => item.trim())
|
|
877
|
+
.filter(Boolean);
|
|
878
|
+
ctx.effect(() => {
|
|
879
|
+
ctx.tools.register(defineTool({
|
|
880
|
+
name: "lume_contract",
|
|
881
|
+
description: "写下或更新本任务的任务契约(需求量化的落点):目标、范围、数量、完成判据、非目标、待确认。任务型请求开工前调用一次;探索后回填实际数量;之后只传变化的字段即可(局部更新)。",
|
|
882
|
+
parameters: {
|
|
883
|
+
goal: { type: "string", description: "目标:一句话、可观察的结果" },
|
|
884
|
+
scope: { type: "string", description: "范围:路径/模块/章节,分号或换行分隔" },
|
|
885
|
+
expectCount: { type: "number", description: "预计数量(探索前先估)" },
|
|
886
|
+
actualCount: { type: "number", description: "实际数量(探索后回填)" },
|
|
887
|
+
criteria: { type: "string", description: "完成判据:可执行、可核对,分号或换行分隔" },
|
|
888
|
+
nonGoals: { type: "string", description: "非目标:明确不动的东西,分号分隔" },
|
|
889
|
+
open: { type: "string", description: "待确认:只列真正阻塞的(≤2 个),分号分隔" },
|
|
890
|
+
},
|
|
891
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记录任务契约" }] },
|
|
892
|
+
execute: async (args, exec) => {
|
|
893
|
+
if (!project)
|
|
894
|
+
throw new Error("lume project store is unavailable");
|
|
895
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
896
|
+
if (!sid)
|
|
897
|
+
throw new Error("lume_contract requires an active session");
|
|
898
|
+
const st = runtime.get(sid);
|
|
899
|
+
const normalized = normalizeContract({
|
|
900
|
+
goal: args.goal,
|
|
901
|
+
scope: splitList(args.scope),
|
|
902
|
+
expectCount: args.expectCount,
|
|
903
|
+
actualCount: args.actualCount,
|
|
904
|
+
criteria: splitList(args.criteria),
|
|
905
|
+
nonGoals: splitList(args.nonGoals),
|
|
906
|
+
open: splitList(args.open),
|
|
907
|
+
}, Date.now(), st.turnIndex);
|
|
908
|
+
const existing = project.getContract(sid);
|
|
909
|
+
if (existing) {
|
|
910
|
+
// 局部更新:未传的字段保持原值(回填数量时不该把判据清空)。
|
|
911
|
+
const patch = {};
|
|
912
|
+
if (args.goal !== undefined)
|
|
913
|
+
patch.goal = normalized.goal;
|
|
914
|
+
if (args.scope !== undefined)
|
|
915
|
+
patch.scope = normalized.scope;
|
|
916
|
+
if (args.expectCount !== undefined)
|
|
917
|
+
patch.expectCount = normalized.expectCount;
|
|
918
|
+
if (args.actualCount !== undefined)
|
|
919
|
+
patch.actualCount = normalized.actualCount;
|
|
920
|
+
if (args.criteria !== undefined)
|
|
921
|
+
patch.criteria = normalized.criteria;
|
|
922
|
+
if (args.nonGoals !== undefined)
|
|
923
|
+
patch.nonGoals = normalized.nonGoals;
|
|
924
|
+
if (args.open !== undefined)
|
|
925
|
+
patch.open = normalized.open;
|
|
926
|
+
await project.patchContract(sid, patch);
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
if (!normalized.goal)
|
|
930
|
+
throw new Error("lume_contract requires a goal on first write");
|
|
931
|
+
await project.setContract(sid, normalized);
|
|
932
|
+
}
|
|
933
|
+
return { ok: true };
|
|
934
|
+
},
|
|
935
|
+
}));
|
|
936
|
+
ctx.tools.register(defineTool({
|
|
937
|
+
name: "lume_change",
|
|
938
|
+
description: "改动台账:记录/更新一处将要改或已改的位置(文件/符号/文档章节 → 改什么 → 怎么验 → 状态)。动手前先列计划项,改完推进状态;只推进状态时可只传 target + status。文档任务用章节名当 target,形成分节记账。",
|
|
939
|
+
parameters: {
|
|
940
|
+
target: { type: "string", required: true, description: "目标位置:文件路径 / 符号 / 文档章节" },
|
|
941
|
+
change: { type: "string", description: "改什么(一句话)" },
|
|
942
|
+
why: { type: "string", description: "为什么改(对齐契约的哪一条)" },
|
|
943
|
+
verify: { type: "string", description: "怎么验(命令 / 回读 / 对照)" },
|
|
944
|
+
status: { type: "string", description: "planned | done | verified | skipped" },
|
|
945
|
+
},
|
|
946
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新改动台账" }] },
|
|
947
|
+
execute: async (args, exec) => {
|
|
948
|
+
if (!project)
|
|
949
|
+
throw new Error("lume project store is unavailable");
|
|
950
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
951
|
+
if (!sid)
|
|
952
|
+
throw new Error("lume_change requires an active session");
|
|
953
|
+
const target = String(args.target ?? "").trim();
|
|
954
|
+
if (!target)
|
|
955
|
+
throw new Error("lume_change requires a target");
|
|
956
|
+
const status = args.status;
|
|
957
|
+
const allowed = status === "planned" || status === "done" || status === "verified" || status === "skipped" ? status : undefined;
|
|
958
|
+
if (args.change === undefined && allowed !== undefined) {
|
|
959
|
+
const hit = await project.setChangeStatus(sid, target, allowed);
|
|
960
|
+
if (!hit)
|
|
961
|
+
throw new Error(`lume_change: no ledger entry for ${target}`);
|
|
962
|
+
return { ok: true };
|
|
963
|
+
}
|
|
964
|
+
const item = normalizeChange({ target, change: args.change, why: args.why, verify: args.verify, status: allowed }, Date.now());
|
|
965
|
+
if (!item)
|
|
966
|
+
throw new Error("lume_change requires target and change");
|
|
967
|
+
await project.upsertChange(sid, item);
|
|
968
|
+
return { ok: true };
|
|
969
|
+
},
|
|
970
|
+
}));
|
|
971
|
+
ctx.tools.register(defineTool({
|
|
972
|
+
name: "lume_hypothesis",
|
|
973
|
+
description: "假设台账:记录一条正在验证的假设及其证据与状态(open/testing/confirmed/excluded)。排查类任务里每验证一次就更新状态;已排除的假设不要再重复尝试。",
|
|
974
|
+
parameters: {
|
|
975
|
+
text: { type: "string", required: true, description: "假设内容,一句话" },
|
|
976
|
+
evidence: { type: "string", description: "支持或推翻它的观察(含命令输出/时间戳摘要)" },
|
|
977
|
+
status: { type: "string", description: "open | testing | confirmed | excluded" },
|
|
978
|
+
},
|
|
979
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新假设台账" }] },
|
|
980
|
+
execute: async (args, exec) => {
|
|
981
|
+
if (!project)
|
|
982
|
+
throw new Error("lume project store is unavailable");
|
|
983
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
984
|
+
if (!sid)
|
|
985
|
+
throw new Error("lume_hypothesis requires an active session");
|
|
986
|
+
const item = normalizeHypothesis({ text: args.text, evidence: args.evidence, status: args.status }, Date.now());
|
|
987
|
+
if (!item)
|
|
988
|
+
throw new Error("lume_hypothesis requires text");
|
|
989
|
+
await project.upsertHypothesis(sid, item);
|
|
990
|
+
runtime.get(sid).hypothesesTouched = true;
|
|
991
|
+
return { ok: true };
|
|
992
|
+
},
|
|
993
|
+
}));
|
|
994
|
+
ctx.tools.register(defineTool({
|
|
995
|
+
name: "lume_project_note",
|
|
996
|
+
description: "记录一条**稳定的项目事实**(按工作目录跨会话累积):构建/测试命令、模块数据流、仓库约定、或一条死路(试过但行不通的做法)。只记可复用、已验证的事实,不要记一次性进展。",
|
|
997
|
+
parameters: {
|
|
998
|
+
kind: { type: "string", required: true, description: "build | test | module | convention | deadend" },
|
|
999
|
+
text: { type: "string", required: true, description: "事实本身,一句话,≤200 字" },
|
|
1000
|
+
},
|
|
1001
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记入项目知识" }] },
|
|
1002
|
+
execute: async (args, exec) => {
|
|
1003
|
+
if (!project)
|
|
1004
|
+
throw new Error("lume project store is unavailable");
|
|
1005
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
1006
|
+
if (!sid)
|
|
1007
|
+
throw new Error("lume_project_note requires an active session");
|
|
1008
|
+
const fact = normalizeProjectFact({ kind: args.kind, text: args.text }, Date.now());
|
|
1009
|
+
if (!fact)
|
|
1010
|
+
throw new Error("lume_project_note requires text");
|
|
1011
|
+
await project.addFact(projectKeyFor(sid, { agent: exec?.agent }), fact, (candidate, existing) => existing.some((entry) => jaccard(entry.text, candidate) >= 0.7));
|
|
1012
|
+
return { ok: true };
|
|
1013
|
+
},
|
|
1014
|
+
}));
|
|
1015
|
+
}, "lume: carrier tools");
|
|
660
1016
|
}, "lume: persona tools");
|
|
661
1017
|
// ── 人设五段式注入 + 切换播报 ──
|
|
662
1018
|
/**
|
|
@@ -702,10 +1058,37 @@ export function apply(ctx, config = {}) {
|
|
|
702
1058
|
}
|
|
703
1059
|
return { text: st.intent?.text ?? st.lastQuery ?? "", mode: st.interactionMode };
|
|
704
1060
|
}
|
|
705
|
-
function
|
|
706
|
-
if (!currentStore)
|
|
707
|
-
return "";
|
|
1061
|
+
function computeTurn(sid, context) {
|
|
708
1062
|
const st = runtime.get(sid);
|
|
1063
|
+
// 意图取自会话权威历史(resolveIntent),不再读事件缓存里的最新文本:
|
|
1064
|
+
// 后者会被注入消息覆盖,且首步可能还带着上一轮的值,导致模式在轮内漂移。
|
|
1065
|
+
const { text: query, mode } = resolveIntent(context, st);
|
|
1066
|
+
// 协议正文按模型能力冻结(不随 query 切变体):切变体会让系统提示词每轮改写,
|
|
1067
|
+
// 代价远超省下的几百 token。见 selectStableThinkingProtocol。
|
|
1068
|
+
const thinkingStable = selectStableThinkingProtocol({
|
|
1069
|
+
isReasoningModel: typeof llmRoute?.model === "string" && REASONING_MODEL_RE.test(llmRoute.model),
|
|
1070
|
+
}).trim();
|
|
1071
|
+
// 易变的任务指令:路由、阶段、闲聊声明、长会话护栏、目标锚点、即时对齐、
|
|
1072
|
+
// 交付复核、压缩重锚、文档能力指引、失败纠偏、反思提醒——全部每步可变。
|
|
1073
|
+
// 载具与方法块(契约/台账/假设/项目知识/影响面/文档方法/触发器提醒)排在最后:
|
|
1074
|
+
// 它们是「此刻最该看的」,紧贴尾部注意力最强位;超预算时先丢可丢块(composeBlocks)。
|
|
1075
|
+
const thinkingRuntime = composeBlocks([
|
|
1076
|
+
{ text: buildInteractionDirective(mode) },
|
|
1077
|
+
{ text: buildTaskPhaseDirective(st.taskPhase) },
|
|
1078
|
+
{ text: buildCasualDirective(TASK_SIGNAL_RE.test(query)) },
|
|
1079
|
+
{ text: buildLongSessionGuard(st.turnIndex) },
|
|
1080
|
+
{ text: buildSessionAnchor(st.turnIndex, mode, query, st.recentTurns) },
|
|
1081
|
+
{ text: st.alignmentCorrection },
|
|
1082
|
+
{ text: st.postTurnReview },
|
|
1083
|
+
{ text: st.compaction ? buildCompactionNotice(st.compaction, st.turnIndex) : null },
|
|
1084
|
+
{ text: st.protocolCorrection },
|
|
1085
|
+
{ text: reflectionStore?.getFeedback() ?? null, droppable: true },
|
|
1086
|
+
...carrierBlocks(sid, context, st, query, mode),
|
|
1087
|
+
]);
|
|
1088
|
+
// 会话选择尚未就绪(启动竞态):只出任务协议,人设段留空——与旧实现一致,
|
|
1089
|
+
// 也避免把「尚未选择」误记成一次人设切换。
|
|
1090
|
+
if (!currentStore)
|
|
1091
|
+
return { thinkingStable, thinkingRuntime, personaStable: "", personaData: "", boundary: "" };
|
|
709
1092
|
const selected = currentStore.get(sid);
|
|
710
1093
|
const personaName = selected ?? defaultName;
|
|
711
1094
|
const previous = st.lastInjected;
|
|
@@ -726,57 +1109,51 @@ export function apply(ctx, config = {}) {
|
|
|
726
1109
|
const boundaryText = inWindow && st.switchTurn !== null
|
|
727
1110
|
? composeBoundary({ registry, previous: st.prevPersona, current: personaName, greeting, escalated: st.leakEscalated })
|
|
728
1111
|
: null;
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
const
|
|
733
|
-
const
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
// 记忆写入都会改变指纹而自动失效。
|
|
739
|
-
const last = (list, field) => list.length > 0 ? list[list.length - 1]?.[field] : "";
|
|
740
|
-
const key = [
|
|
741
|
-
String(personaName),
|
|
742
|
-
String(persona?.name ?? ""),
|
|
1112
|
+
st.lastInjected = personaName;
|
|
1113
|
+
// 恒定段:只依赖「谁在当值 + 契约正文 + 身份名」。检索结果、示例、播报都进易变段。
|
|
1114
|
+
const profileName = personaName ? registry.profileNameOf(personaName) : null;
|
|
1115
|
+
const personaStable = buildPersonaContractSection({ persona, profileName });
|
|
1116
|
+
const personaData = buildPersonaRuntimeSection({
|
|
1117
|
+
persona,
|
|
1118
|
+
memories: personaName ? identity?.getMemory(personaName) ?? [] : [],
|
|
1119
|
+
styleRules: personaName ? identity?.getStyleRules(personaName) ?? [] : [],
|
|
1120
|
+
corpusPins: personaName ? identity?.getCorpusPins(personaName) ?? [] : [],
|
|
743
1121
|
query,
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
String(
|
|
756
|
-
strategy,
|
|
757
|
-
].join("\u0001");
|
|
758
|
-
let text = st.personaCache !== null && st.personaCache.key === key ? st.personaCache.text : null;
|
|
759
|
-
if (text === null) {
|
|
760
|
-
text = buildPersonaSection({
|
|
761
|
-
persona,
|
|
762
|
-
profileName: personaName ? registry.profileNameOf(personaName) : null,
|
|
763
|
-
memories,
|
|
764
|
-
styleRules,
|
|
765
|
-
corpusPins,
|
|
766
|
-
query,
|
|
767
|
-
turnIndex: st.turnIndex,
|
|
768
|
-
sessionKey: sid,
|
|
769
|
-
boundaryText: null,
|
|
770
|
-
config: { sampleCount, sampleMin, memoryInject, styleInject, strategy },
|
|
771
|
-
});
|
|
772
|
-
st.personaCache = { turnIndex: st.turnIndex, key, text };
|
|
1122
|
+
turnIndex: st.turnIndex,
|
|
1123
|
+
sessionKey: sid,
|
|
1124
|
+
// 播报单独成段(lume:boundary),不混进数据段——便于单独观测与测试
|
|
1125
|
+
boundaryText: null,
|
|
1126
|
+
config: { sampleCount, sampleMin, memoryInject, styleInject, strategy },
|
|
1127
|
+
});
|
|
1128
|
+
// 现场可观测:恒定段指纹每变一次写一行诊断。健康会话只会留下 1-2 行
|
|
1129
|
+
// (首次 + 人设切换);若长会话里反复出现,说明又有东西混进了 system 段。
|
|
1130
|
+
const digest = fnv1a32(`${thinkingStable}\u0001${personaStable}`).toString(16);
|
|
1131
|
+
if (st.stableDigest !== digest) {
|
|
1132
|
+
st.stableDigest = digest;
|
|
1133
|
+
appendLumeLog(`[${sid}] 系统段指纹 ${digest}(当值=${String(personaName)},身份=${String(profileName)},通道=${layeredOn ? "runtime-context" : "system(降级)"})`);
|
|
773
1134
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1135
|
+
return { thinkingStable, thinkingRuntime, personaStable, personaData, boundary: boundaryText ?? "" };
|
|
1136
|
+
}
|
|
1137
|
+
/**
|
|
1138
|
+
* system 段取文:恒定段 +(宿主不支持 runtime-context 时)易变段。
|
|
1139
|
+
* 降级时并回 system 是刻意的取舍——宁可丢前缀缓存,也不能丢记忆与播报注入。
|
|
1140
|
+
*/
|
|
1141
|
+
function systemSectionText(sid, context, part) {
|
|
1142
|
+
const turn = computeTurn(sid, context);
|
|
1143
|
+
if (layeredOn)
|
|
1144
|
+
return part === "thinking" ? turn.thinkingStable : turn.personaStable;
|
|
1145
|
+
const stable = part === "thinking" ? turn.thinkingStable : turn.personaStable;
|
|
1146
|
+
const dynamic = part === "thinking"
|
|
1147
|
+
? turn.thinkingRuntime
|
|
1148
|
+
: [turn.personaData, turn.boundary].filter(Boolean).join("\n\n");
|
|
1149
|
+
return [stable, dynamic].filter(Boolean).join("\n\n");
|
|
1150
|
+
}
|
|
1151
|
+
/** 易变段取文:任务指令 / 人设数据 / 切换播报三段各自成 context,由宿主按 order 归并成一条尾部快照。 */
|
|
1152
|
+
function runtimeContextText(sid, context, part) {
|
|
1153
|
+
const turn = computeTurn(sid, context);
|
|
1154
|
+
if (part === "thinking")
|
|
1155
|
+
return turn.thinkingRuntime;
|
|
1156
|
+
return part === "persona" ? turn.personaData : turn.boundary;
|
|
780
1157
|
}
|
|
781
1158
|
// ── RPC 通道 ──
|
|
782
1159
|
const handleEndpoint = createLumeRpcHandler({
|
|
@@ -795,6 +1172,29 @@ export function apply(ctx, config = {}) {
|
|
|
795
1172
|
get distill() {
|
|
796
1173
|
return distillRunner;
|
|
797
1174
|
},
|
|
1175
|
+
getProjectState(sessionId) {
|
|
1176
|
+
// 诊断视图:任务载具 + 项目知识(供排查"模型到底看到了什么")。
|
|
1177
|
+
if (!project)
|
|
1178
|
+
return null;
|
|
1179
|
+
const st = runtime.get(sessionId);
|
|
1180
|
+
return {
|
|
1181
|
+
projectKey: st.projectKey,
|
|
1182
|
+
contract: project.getContract(sessionId),
|
|
1183
|
+
changes: project.getChanges(sessionId),
|
|
1184
|
+
hypotheses: project.getHypotheses(sessionId),
|
|
1185
|
+
facts: st.projectKey ? project.getFacts(st.projectKey) : [],
|
|
1186
|
+
triggers: { ...st.triggerCounters, fired: st.triggerFiredAt },
|
|
1187
|
+
};
|
|
1188
|
+
},
|
|
1189
|
+
async clearProjectFacts(sessionId) {
|
|
1190
|
+
if (!project)
|
|
1191
|
+
return false;
|
|
1192
|
+
const st = runtime.get(sessionId);
|
|
1193
|
+
if (!st.projectKey)
|
|
1194
|
+
return false;
|
|
1195
|
+
await project.clearFacts(st.projectKey);
|
|
1196
|
+
return true;
|
|
1197
|
+
},
|
|
798
1198
|
});
|
|
799
1199
|
ctx.effect(() => ctx.connection.rpc.handle(LUME_CHANNEL, async (endpoint, payload) => {
|
|
800
1200
|
currentStore ??= await storesReady;
|
|
@@ -811,57 +1211,36 @@ export function apply(ctx, config = {}) {
|
|
|
811
1211
|
order: personaOrder,
|
|
812
1212
|
text: (context) => {
|
|
813
1213
|
const sid = context.agent?.session?.id ?? context.agent?.id;
|
|
814
|
-
return sid ?
|
|
1214
|
+
return sid ? systemSectionText(String(sid), context, "persona") : "";
|
|
815
1215
|
},
|
|
816
1216
|
}), "lume.persona-section()");
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1217
|
+
// 易变段走 runtime-context 通道:宿主把它渲染成对话尾部的一条快照消息(文案自带
|
|
1218
|
+
// "supersedes earlier runtime-context snapshots",新快照取代旧快照,不堆叠进历史),
|
|
1219
|
+
// 因此它的每一次变化只花自己那几百 token,不作废前面的任何前缀。
|
|
1220
|
+
// 宿主不支持该 API 时 layeredOn=false,易变段已并回 system 段(见 systemSectionText)。
|
|
1221
|
+
if (layeredOn) {
|
|
1222
|
+
const dynamicContexts = [
|
|
1223
|
+
{ name: LUME_RUNTIME_CONTEXT, order: LUME_RUNTIME_ORDER, part: "thinking" },
|
|
1224
|
+
{ name: LUME_PERSONA_RUNTIME_CONTEXT, order: LUME_PERSONA_RUNTIME_ORDER, part: "persona" },
|
|
1225
|
+
{ name: LUME_BOUNDARY_CONTEXT, order: LUME_BOUNDARY_ORDER, part: "boundary" },
|
|
1226
|
+
];
|
|
1227
|
+
for (const entry of dynamicContexts) {
|
|
1228
|
+
ctx.effect(() => ctx.systemPrompt.context({
|
|
1229
|
+
name: entry.name,
|
|
1230
|
+
order: entry.order,
|
|
1231
|
+
text: (context) => {
|
|
1232
|
+
const sid = context.agent?.session?.id ?? context.agent?.id;
|
|
1233
|
+
return sid ? runtimeContextText(String(sid), context, entry.part) : "";
|
|
1234
|
+
},
|
|
1235
|
+
}), `lume.runtime-context(${entry.name})`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
827
1238
|
ctx.effect(() => ctx.systemPrompt.section({
|
|
828
1239
|
name: LUME_THINKING_SECTION,
|
|
829
1240
|
order: LUME_THINKING_ORDER,
|
|
830
1241
|
text: (context) => {
|
|
831
1242
|
const sid = context.agent?.session?.id ?? context.agent?.id;
|
|
832
|
-
|
|
833
|
-
// 意图取自会话权威历史(resolveIntent),不再读事件缓存里的最新文本:
|
|
834
|
-
// 后者会被注入消息覆盖,且首步可能还带着上一轮的值,导致模式在轮内漂移。
|
|
835
|
-
const intent = st !== null && sid ? resolveIntent(context, st) : null;
|
|
836
|
-
const query = intent?.text ?? "";
|
|
837
|
-
// 已知推理型模型具备计划能力,任务轮只保留变更、验证、归因和复核约束;
|
|
838
|
-
// 路由未知时使用完整版,避免误判造成能力退化。
|
|
839
|
-
const base = selectThinkingProtocol({
|
|
840
|
-
isTask: TASK_SIGNAL_RE.test(query),
|
|
841
|
-
isReasoningModel: typeof llmRoute?.model === "string" && REASONING_MODEL_RE.test(llmRoute.model),
|
|
842
|
-
});
|
|
843
|
-
// 纠偏与反思提醒属于任务协议,跟人设无关——挂在 thinking 段
|
|
844
|
-
// 才能在「不使用人设」的纯任务会话里也生效。
|
|
845
|
-
const correction = st?.protocolCorrection;
|
|
846
|
-
const reflectionHint = reflectionStore?.getFeedback() ?? null;
|
|
847
|
-
const mode = intent?.mode ?? "question";
|
|
848
|
-
const route = buildInteractionDirective(mode);
|
|
849
|
-
const longSession = buildLongSessionGuard(st?.turnIndex ?? 0);
|
|
850
|
-
const anchor = buildSessionAnchor(st?.turnIndex ?? 0, mode, query, st?.recentTurns ?? []);
|
|
851
|
-
const alignment = st?.alignmentCorrection;
|
|
852
|
-
const postTurnReview = st?.postTurnReview;
|
|
853
|
-
const phase = buildTaskPhaseDirective(st?.taskPhase ?? "answer");
|
|
854
|
-
// 文档能力感知:DSH 本身不带 Office/PDF 读写能力,第三方文档工具插件
|
|
855
|
-
// 装与不装由用户决定。按探测结果分叉——有工具就约束走工具与回读验证,
|
|
856
|
-
// 没工具就约束「如实说明边界、不要硬解二进制」。只在文档任务轮注入,
|
|
857
|
-
// 且判据取自冻结的意图文本,因此轮内稳定,不会在轮中作废前缀缓存。
|
|
858
|
-
const document = buildDocumentDirective({ query, capabilities: probeDocumentCapabilities(ctx.tools, context.agent) });
|
|
859
|
-
// 工具失败提示已移到 runtime-context 通道(见 lume.tool-notice-context):
|
|
860
|
-
// 它一出现就固定不变,但会变的内容不该待在 system 串里——那会作废前缀缓存。
|
|
861
|
-
// 压缩重锚:宿主的 preset 隔离域负责压缩,Lume 只能观察事件;
|
|
862
|
-
// 在压缩后一轮提醒「摘要不是完整历史」。
|
|
863
|
-
const compactionNotice = st?.compaction ? buildCompactionNotice(st.compaction, st.turnIndex) : null;
|
|
864
|
-
return [base, route, phase, document, longSession, anchor, alignment, postTurnReview, compactionNotice, correction, reflectionHint].filter(Boolean).join("\n\n");
|
|
1243
|
+
return sid ? systemSectionText(String(sid), context, "thinking") : "";
|
|
865
1244
|
},
|
|
866
1245
|
}), "lume.thinking-section()");
|
|
867
1246
|
// 工具失败提示走 runtime-context 通道:宿主把它渲染成对话尾部的一条消息,
|