lume-dsh-plugin 0.6.2 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -5
- package/lib/core/ledger.js +211 -0
- package/lib/core/signals.js +64 -0
- package/lib/host/methods.js +64 -0
- package/lib/host/project.js +222 -0
- package/lib/host/reflection.js +27 -6
- package/lib/host/rpc.js +15 -0
- package/lib/host/session-runtime.js +10 -0
- package/lib/host/triggers.js +133 -0
- package/lib/index.js +394 -25
- package/package.json +2 -6
package/lib/index.js
CHANGED
|
@@ -38,6 +38,11 @@ import { appendLumeLog } from "./host/diag.js";
|
|
|
38
38
|
import { advancePhase, buildAlignmentCorrection, buildCasualDirective, buildCompactionNotice, buildInteractionDirective, buildLongSessionGuard, buildSessionAnchor, buildTaskPhaseDirective, buildToolFailureNotice, classifyInteraction, isUserAuthored, taskPhaseForMode } from "./host/protocol.js";
|
|
39
39
|
import { buildDocumentDirective, probeDocumentCapabilities } from "./host/documents.js";
|
|
40
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";
|
|
41
46
|
/** schemastery → domainTable 形参的桥接(与 identity.ts 同款)。 */
|
|
42
47
|
const recordSchema = zodLike;
|
|
43
48
|
/** 会话人设选择的持久层(键 = sessionId)。 */
|
|
@@ -97,7 +102,24 @@ const SWITCH_BOUNDARY_TURNS = 2;
|
|
|
97
102
|
export const name = "lume";
|
|
98
103
|
/** 依赖的服务 */
|
|
99
104
|
export const inject = ["systemPrompt", "connection", "storageDomain", "tools", "llm", "agentDefaultModel", "settings"];
|
|
105
|
+
/**
|
|
106
|
+
* 插件入口。**外层只做兜底**:任何宿主 API 变更都不该让 DSH 起不来。
|
|
107
|
+
*
|
|
108
|
+
* 0.7.0 的真实教训:新宿主(DSH Desktop 0.9.x / 宿主包 0.1.5-rc.2)里
|
|
109
|
+
* `connection.rpc.handle` 内部会以**调用方**的 ctx 去 `owner.webServer.register(...)`,
|
|
110
|
+
* 未注入 webServer 时 cordis 抛 "cannot get property \"webServer\" without inject";
|
|
111
|
+
* 插件 apply 抛错 → 整个插件树加载失败 → `DSH entry failed`,用户连界面都进不去。
|
|
112
|
+
* 把异常圈在插件内部,降级成「部分功能不可用」远比「宿主起不来」可接受。
|
|
113
|
+
*/
|
|
100
114
|
export function apply(ctx, config = {}) {
|
|
115
|
+
try {
|
|
116
|
+
applyInner(ctx, config);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
ctx?.logger?.error?.("lume: 初始化失败,已降级(不影响 DSH 启动;请升级插件或反馈此错误)", error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function applyInner(ctx, config = {}) {
|
|
101
123
|
const assetsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "assets");
|
|
102
124
|
const builtins = loadPersonalities(assetsDir);
|
|
103
125
|
const sampleCount = config.sampleCount ?? 6;
|
|
@@ -186,6 +208,35 @@ export function apply(ctx, config = {}) {
|
|
|
186
208
|
}
|
|
187
209
|
})();
|
|
188
210
|
void reflectionReady.then((s) => { reflectionStore = s; });
|
|
211
|
+
// ── 项目域:任务契约 / 改动台账 / 假设台账 / 项目知识(失败降级为无载具功能)──
|
|
212
|
+
const projectMemoryOn = config.projectMemory ?? true;
|
|
213
|
+
const behaviorTriggersOn = config.behaviorTriggers ?? true;
|
|
214
|
+
const triggerThresholds = {
|
|
215
|
+
...DEFAULT_TRIGGER_THRESHOLDS,
|
|
216
|
+
inspectStreak: config.triggerInspectStreak ?? DEFAULT_TRIGGER_THRESHOLDS.inspectStreak,
|
|
217
|
+
changeStreak: config.triggerChangeStreak ?? DEFAULT_TRIGGER_THRESHOLDS.changeStreak,
|
|
218
|
+
deadPathFails: config.triggerDeadPathFails ?? DEFAULT_TRIGGER_THRESHOLDS.deadPathFails,
|
|
219
|
+
};
|
|
220
|
+
let project = null;
|
|
221
|
+
const projectReady = (async () => {
|
|
222
|
+
if (!projectMemoryOn)
|
|
223
|
+
return null;
|
|
224
|
+
try {
|
|
225
|
+
const domain = await ctx.storageDomain.open(LUME_PROJECT_SPEC);
|
|
226
|
+
ctx.effect(() => async () => { await domain.close(); }, "lume: close project domain");
|
|
227
|
+
return new ProjectStore({
|
|
228
|
+
contract: domain.table("contract"),
|
|
229
|
+
ledger: domain.table("ledger"),
|
|
230
|
+
hypotheses: domain.table("hypotheses"),
|
|
231
|
+
facts: domain.table("facts"),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
ctx.logger?.warn?.("lume: 项目域不可用,任务契约/台账/项目知识降级", error);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
})();
|
|
239
|
+
void projectReady.then((s) => { project = s; });
|
|
189
240
|
const registry = new PersonaRegistry(builtins, () => identity);
|
|
190
241
|
ctx.logger?.warn?.(`lume: 已加载(builtins=${Object.keys(builtins).join(",") || "空!"},assets=${assetsDir})`);
|
|
191
242
|
ctx.logger?.warn?.(`lume: llmRoute 初始化策略:agentDefaultModel → settings → 回退`);
|
|
@@ -511,6 +562,9 @@ export function apply(ctx, config = {}) {
|
|
|
511
562
|
st.toolCalls++;
|
|
512
563
|
if (st.interactionMode === "execute")
|
|
513
564
|
st.taskPhase = advancePhase(st.taskPhase, "execute");
|
|
565
|
+
// 行为类别在调用阶段记账(连击),成败到结果阶段才结算。
|
|
566
|
+
st.toolKind = classifyTool(event.data?.name);
|
|
567
|
+
applyToolSignal(st.triggerCounters, st.toolKind, null);
|
|
514
568
|
break;
|
|
515
569
|
}
|
|
516
570
|
case "tool/result": {
|
|
@@ -526,6 +580,30 @@ export function apply(ctx, config = {}) {
|
|
|
526
580
|
st.toolSuccesses++;
|
|
527
581
|
if (st.interactionMode === "execute")
|
|
528
582
|
st.taskPhase = advancePhase(st.taskPhase, unknownResult || explicitError ? "diagnose" : "verify");
|
|
583
|
+
// 行为信号 → 计数器 → 触发器提醒。提醒只在这一步之后可见(尾部快照),
|
|
584
|
+
// 且每类触发器有轮级冷却:提示一多就变噪音,模型会学会忽略。
|
|
585
|
+
const signals = readResultSignals(resultText, explicitError);
|
|
586
|
+
applyVerifyOutcome(st.triggerCounters, st.toolKind, signals);
|
|
587
|
+
if (behaviorTriggersOn) {
|
|
588
|
+
const fire = evaluateToolTrigger(st.triggerCounters, {
|
|
589
|
+
turnIndex: st.turnIndex,
|
|
590
|
+
isTask: isTaskQuery(st),
|
|
591
|
+
diagnosing: st.interactionMode === "diagnosis",
|
|
592
|
+
hasContract: contractOf(sid) !== null,
|
|
593
|
+
hasHypotheses: hypothesesOf(sid).length > 0,
|
|
594
|
+
hypothesesTouched: st.hypothesesTouched,
|
|
595
|
+
}, triggerThresholds);
|
|
596
|
+
if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
597
|
+
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
598
|
+
st.triggerNudge = fire.text;
|
|
599
|
+
ctx.logger?.warn?.(`lume: [${sid}] 行为触发器 ${fire.id}(steps=${st.triggerCounters.steps},inspect=${st.triggerCounters.inspectStreak},mutate=${st.triggerCounters.mutateStreak},verifyFail=${st.triggerCounters.verifyFailStreak})`);
|
|
600
|
+
// 环境性死路自动落成项目知识:下次会话不必重踩。
|
|
601
|
+
if (fire.id === "dead-path" && signals.env && project) {
|
|
602
|
+
const key = projectKeyFor(sid, session);
|
|
603
|
+
void project.addFact(key, normalizeProjectFact({ kind: "deadend", text: `本环境验证受阻(${st.triggerCounters.verifyFailStreak} 次连续失败,环境/依赖类):换降级阶梯,不要重复同一命令` }, Date.now()), (candidate, existing) => existing.some((fact) => fact.text === candidate));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
529
607
|
break;
|
|
530
608
|
}
|
|
531
609
|
case "compaction/summary": {
|
|
@@ -546,6 +624,37 @@ export function apply(ctx, config = {}) {
|
|
|
546
624
|
st.switchGreetingPending = false;
|
|
547
625
|
if (st.switchTurn !== null && st.turnIndex - st.switchTurn >= boundaryTurns)
|
|
548
626
|
st.switchTurn = null;
|
|
627
|
+
// ── 行为触发器(轮边界)──
|
|
628
|
+
// 连击按轮清零:新一轮是新请求,上一轮的「撒网」不该继续累加;死路连击
|
|
629
|
+
// 跨轮保留(同一环境不可用是会话级事实)。上轮的提醒到这里失效。
|
|
630
|
+
st.triggerCounters.inspectStreak = 0;
|
|
631
|
+
st.triggerCounters.mutateStreak = 0;
|
|
632
|
+
st.triggerNudge = null;
|
|
633
|
+
st.turnNudge = null;
|
|
634
|
+
st.hypothesesTouched = false;
|
|
635
|
+
if (behaviorTriggersOn && project) {
|
|
636
|
+
const fire = evaluateTurnTrigger({
|
|
637
|
+
turnIndex: st.turnIndex,
|
|
638
|
+
hasContract: contractOf(sid) !== null,
|
|
639
|
+
compactionTurn: st.compaction?.turnIndex ?? null,
|
|
640
|
+
lastDriftTurn: st.lastDriftTurn,
|
|
641
|
+
counters: st.triggerCounters,
|
|
642
|
+
knowledgePrompted: st.knowledgePrompted,
|
|
643
|
+
}, triggerThresholds);
|
|
644
|
+
if (fire && cooldownOk(st.triggerFiredAt[fire.id], st.turnIndex)) {
|
|
645
|
+
st.triggerFiredAt[fire.id] = st.turnIndex;
|
|
646
|
+
if (fire.id === "criteria-drift") {
|
|
647
|
+
// 契约对账用「交付口径」渲染原始判据:防判据随进展漂移。
|
|
648
|
+
st.lastDriftTurn = st.turnIndex;
|
|
649
|
+
st.turnNudge = renderContract(contractOf(sid), true);
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
st.knowledgePrompted = true;
|
|
653
|
+
st.turnNudge = fire.text;
|
|
654
|
+
}
|
|
655
|
+
ctx.logger?.warn?.(`lume: [${sid}] 轮触发器 ${fire.id}(turn=${st.turnIndex})`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
549
658
|
// 低成本会话内纠偏:只处理明确的错误/失败信号,且要求连续轮次用户请求相同。
|
|
550
659
|
const failed = /失败|报错|错误|exception|traceback|cannot|unable|permission denied|timed out|找不到|不存在/i.test(st.assistantText);
|
|
551
660
|
const queryKey = st.userText.trim().replace(/\s+/g, " ").slice(0, 240);
|
|
@@ -602,6 +711,8 @@ export function apply(ctx, config = {}) {
|
|
|
602
711
|
const st = runtime.get(sid);
|
|
603
712
|
const turns = [...st.recentTurns];
|
|
604
713
|
runtime.delete(sid);
|
|
714
|
+
// 任务载具是会话态:任务结束即无意义,清掉避免无界增长(项目知识在另一张表,不受影响)。
|
|
715
|
+
void projectReady.then((store) => store?.clearSession(sid));
|
|
605
716
|
// 反思日志:会话结束后空闲时间跑一次小模型,零用户感知 token。
|
|
606
717
|
// 历史不够长(< 4 条消息)或路由不可用时静默跳过。
|
|
607
718
|
if (reflectionEnabled && turns.length >= 4) {
|
|
@@ -620,10 +731,89 @@ export function apply(ctx, config = {}) {
|
|
|
620
731
|
if (!score)
|
|
621
732
|
return;
|
|
622
733
|
await store.log(sid, score);
|
|
623
|
-
ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review}「${score.note}」`);
|
|
734
|
+
ctx.logger?.warn?.(`lume: 反思日志 ${sid} context=${score.context} planning=${score.planning} verification=${score.verification} review=${score.review} diagnosis=${score.diagnosis}「${score.note}」`);
|
|
624
735
|
})();
|
|
625
736
|
}
|
|
626
737
|
}), "lume: session disposal");
|
|
738
|
+
// ── 载具与项目知识的读取入口(事件处理器 / 工具 / 注入三处共用)──
|
|
739
|
+
/** 项目键:优先取会话工作目录(跨会话共享同一仓库的知识)。 */
|
|
740
|
+
function projectKeyFor(sid, source) {
|
|
741
|
+
const st = runtime.get(sid);
|
|
742
|
+
if (st.projectKey)
|
|
743
|
+
return st.projectKey;
|
|
744
|
+
// 三种调用来源:提示词 context({agent:{session}})、工具 exec({agent:{session}})、
|
|
745
|
+
// 会话事件(session 本身)。统一取到 session 再读 cwd。
|
|
746
|
+
const session = source?.agent?.session ?? source?.session ?? source;
|
|
747
|
+
const cwd = session?.cwd ?? "";
|
|
748
|
+
const key = projectKeyOf(cwd);
|
|
749
|
+
// 只有拿到真实工作目录才缓存:否则一次无 cwd 的调用会把 "unknown" 固化下来。
|
|
750
|
+
if (cwd)
|
|
751
|
+
st.projectKey = key;
|
|
752
|
+
return key;
|
|
753
|
+
}
|
|
754
|
+
function isTaskQuery(st) {
|
|
755
|
+
return TASK_SIGNAL_RE.test(st.intent?.text ?? st.userText ?? "");
|
|
756
|
+
}
|
|
757
|
+
function contractOf(sid) {
|
|
758
|
+
return project?.getContract(sid) ?? null;
|
|
759
|
+
}
|
|
760
|
+
function changesOf(sid) {
|
|
761
|
+
return project?.getChanges(sid) ?? [];
|
|
762
|
+
}
|
|
763
|
+
function hypothesesOf(sid) {
|
|
764
|
+
return project?.getHypotheses(sid) ?? [];
|
|
765
|
+
}
|
|
766
|
+
function factsOf(sid, context) {
|
|
767
|
+
return project ? project.getFacts(projectKeyFor(sid, context)) : [];
|
|
768
|
+
}
|
|
769
|
+
/** 环境里是否有符号级结构分析工具:有就让模型用它替代通篇 read。 */
|
|
770
|
+
function structureToolName(context) {
|
|
771
|
+
try {
|
|
772
|
+
const schemas = ctx.get("tools")?.schemas?.(context?.agent);
|
|
773
|
+
if (!Array.isArray(schemas))
|
|
774
|
+
return null;
|
|
775
|
+
for (const schema of schemas) {
|
|
776
|
+
const name = String(schema?.name ?? "");
|
|
777
|
+
if (/analy|tree|symbol|lsp|reference|code_map|outline/i.test(name))
|
|
778
|
+
return name;
|
|
779
|
+
}
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* 任务载具块:契约 / 改动台账 / 假设台账 / 项目知识 + 方法块 + 触发器提醒。
|
|
788
|
+
*
|
|
789
|
+
* 全部落在尾部快照(易变层)——这正是「载具」现在才做得起的原因:0.6.2 之前每步
|
|
790
|
+
* 注入一份会变的状态等于每步作废整段前缀,而快照只在内容变化时才付费(实测 58 步
|
|
791
|
+
* 只产生 9 条快照)。顺序上把「此刻最该做的一件事」(触发器提醒)放在最后。
|
|
792
|
+
*/
|
|
793
|
+
function carrierBlocks(sid, context, st, query, mode) {
|
|
794
|
+
if (!projectMemoryOn)
|
|
795
|
+
return [];
|
|
796
|
+
const isTask = TASK_SIGNAL_RE.test(query);
|
|
797
|
+
const contract = contractOf(sid);
|
|
798
|
+
const changes = changesOf(sid);
|
|
799
|
+
const docDirective = buildDocumentDirective({ query, capabilities: probeDocumentCapabilities(ctx.get("tools"), context?.agent) });
|
|
800
|
+
return [
|
|
801
|
+
// 契约:有就回显(交付轮切成对账口径),没有且是任务轮就先教它写一份。
|
|
802
|
+
{ text: renderContract(contract, st.taskPhase === "deliver") },
|
|
803
|
+
{ text: !contract && isTask ? buildContractMethodDirective() : null, droppable: true },
|
|
804
|
+
// 台账与假设:存在就回显——让模型「看见」自己的计划,而不是记在脑子里。
|
|
805
|
+
{ text: changes.length > 0 ? renderChangeLedger(changes) : null },
|
|
806
|
+
{ text: renderHypotheses(hypothesesOf(sid)) },
|
|
807
|
+
// 项目知识:只在与项目相关的轮次出现(闲聊不该背仓库事实)。
|
|
808
|
+
{ text: isTask ? renderProjectFacts(factsOf(sid, context)) : null, droppable: true },
|
|
809
|
+
// 方法块:按任务形态出现;文档方法论只在判定为文档任务时出现。
|
|
810
|
+
{ text: isTask && mode !== "question" ? buildImpactDirective() : null, droppable: true },
|
|
811
|
+
{ text: docDirective ? buildDocumentMethodDirective() : null },
|
|
812
|
+
{ text: mode === "execute" || mode === "diagnosis" ? buildStructureHint(structureToolName(context)) : null, droppable: true },
|
|
813
|
+
{ text: st.turnNudge },
|
|
814
|
+
{ text: st.triggerNudge },
|
|
815
|
+
];
|
|
816
|
+
}
|
|
627
817
|
// ── 模型可调用工具(主写入通道)──
|
|
628
818
|
// 工具 output schema 的 const 语义要求成功值恒为 { ok: true };失败一律抛错交由框架呈现。
|
|
629
819
|
// as const 让 defineTool 从字面量推断 O,三个工具共用同一份成功形状。
|
|
@@ -694,6 +884,152 @@ export function apply(ctx, config = {}) {
|
|
|
694
884
|
return { ok: true };
|
|
695
885
|
},
|
|
696
886
|
}));
|
|
887
|
+
// ── 任务载具工具(第二组写入通道)──
|
|
888
|
+
// 与人格工具一样是「模型主动调用、零额外 LLM 调用」,区别在写入对象:契约/台账/假设
|
|
889
|
+
// 属于当前任务(会话态),项目知识按工作目录跨会话累积。列表类参数统一用字符串
|
|
890
|
+
// 分隔(分号或换行),不引入数组 schema——省 schema token,也少一层校验风险。
|
|
891
|
+
const splitList = (value) => String(value ?? "")
|
|
892
|
+
.split(/[;;\n]/)
|
|
893
|
+
.map((item) => item.trim())
|
|
894
|
+
.filter(Boolean);
|
|
895
|
+
ctx.effect(() => {
|
|
896
|
+
ctx.tools.register(defineTool({
|
|
897
|
+
name: "lume_contract",
|
|
898
|
+
description: "写下或更新本任务的任务契约(需求量化的落点):目标、范围、数量、完成判据、非目标、待确认。任务型请求开工前调用一次;探索后回填实际数量;之后只传变化的字段即可(局部更新)。",
|
|
899
|
+
parameters: {
|
|
900
|
+
goal: { type: "string", description: "目标:一句话、可观察的结果" },
|
|
901
|
+
scope: { type: "string", description: "范围:路径/模块/章节,分号或换行分隔" },
|
|
902
|
+
expectCount: { type: "number", description: "预计数量(探索前先估)" },
|
|
903
|
+
actualCount: { type: "number", description: "实际数量(探索后回填)" },
|
|
904
|
+
criteria: { type: "string", description: "完成判据:可执行、可核对,分号或换行分隔" },
|
|
905
|
+
nonGoals: { type: "string", description: "非目标:明确不动的东西,分号分隔" },
|
|
906
|
+
open: { type: "string", description: "待确认:只列真正阻塞的(≤2 个),分号分隔" },
|
|
907
|
+
},
|
|
908
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记录任务契约" }] },
|
|
909
|
+
execute: async (args, exec) => {
|
|
910
|
+
if (!project)
|
|
911
|
+
throw new Error("lume project store is unavailable");
|
|
912
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
913
|
+
if (!sid)
|
|
914
|
+
throw new Error("lume_contract requires an active session");
|
|
915
|
+
const st = runtime.get(sid);
|
|
916
|
+
const normalized = normalizeContract({
|
|
917
|
+
goal: args.goal,
|
|
918
|
+
scope: splitList(args.scope),
|
|
919
|
+
expectCount: args.expectCount,
|
|
920
|
+
actualCount: args.actualCount,
|
|
921
|
+
criteria: splitList(args.criteria),
|
|
922
|
+
nonGoals: splitList(args.nonGoals),
|
|
923
|
+
open: splitList(args.open),
|
|
924
|
+
}, Date.now(), st.turnIndex);
|
|
925
|
+
const existing = project.getContract(sid);
|
|
926
|
+
if (existing) {
|
|
927
|
+
// 局部更新:未传的字段保持原值(回填数量时不该把判据清空)。
|
|
928
|
+
const patch = {};
|
|
929
|
+
if (args.goal !== undefined)
|
|
930
|
+
patch.goal = normalized.goal;
|
|
931
|
+
if (args.scope !== undefined)
|
|
932
|
+
patch.scope = normalized.scope;
|
|
933
|
+
if (args.expectCount !== undefined)
|
|
934
|
+
patch.expectCount = normalized.expectCount;
|
|
935
|
+
if (args.actualCount !== undefined)
|
|
936
|
+
patch.actualCount = normalized.actualCount;
|
|
937
|
+
if (args.criteria !== undefined)
|
|
938
|
+
patch.criteria = normalized.criteria;
|
|
939
|
+
if (args.nonGoals !== undefined)
|
|
940
|
+
patch.nonGoals = normalized.nonGoals;
|
|
941
|
+
if (args.open !== undefined)
|
|
942
|
+
patch.open = normalized.open;
|
|
943
|
+
await project.patchContract(sid, patch);
|
|
944
|
+
}
|
|
945
|
+
else {
|
|
946
|
+
if (!normalized.goal)
|
|
947
|
+
throw new Error("lume_contract requires a goal on first write");
|
|
948
|
+
await project.setContract(sid, normalized);
|
|
949
|
+
}
|
|
950
|
+
return { ok: true };
|
|
951
|
+
},
|
|
952
|
+
}));
|
|
953
|
+
ctx.tools.register(defineTool({
|
|
954
|
+
name: "lume_change",
|
|
955
|
+
description: "改动台账:记录/更新一处将要改或已改的位置(文件/符号/文档章节 → 改什么 → 怎么验 → 状态)。动手前先列计划项,改完推进状态;只推进状态时可只传 target + status。文档任务用章节名当 target,形成分节记账。",
|
|
956
|
+
parameters: {
|
|
957
|
+
target: { type: "string", required: true, description: "目标位置:文件路径 / 符号 / 文档章节" },
|
|
958
|
+
change: { type: "string", description: "改什么(一句话)" },
|
|
959
|
+
why: { type: "string", description: "为什么改(对齐契约的哪一条)" },
|
|
960
|
+
verify: { type: "string", description: "怎么验(命令 / 回读 / 对照)" },
|
|
961
|
+
status: { type: "string", description: "planned | done | verified | skipped" },
|
|
962
|
+
},
|
|
963
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新改动台账" }] },
|
|
964
|
+
execute: async (args, exec) => {
|
|
965
|
+
if (!project)
|
|
966
|
+
throw new Error("lume project store is unavailable");
|
|
967
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
968
|
+
if (!sid)
|
|
969
|
+
throw new Error("lume_change requires an active session");
|
|
970
|
+
const target = String(args.target ?? "").trim();
|
|
971
|
+
if (!target)
|
|
972
|
+
throw new Error("lume_change requires a target");
|
|
973
|
+
const status = args.status;
|
|
974
|
+
const allowed = status === "planned" || status === "done" || status === "verified" || status === "skipped" ? status : undefined;
|
|
975
|
+
if (args.change === undefined && allowed !== undefined) {
|
|
976
|
+
const hit = await project.setChangeStatus(sid, target, allowed);
|
|
977
|
+
if (!hit)
|
|
978
|
+
throw new Error(`lume_change: no ledger entry for ${target}`);
|
|
979
|
+
return { ok: true };
|
|
980
|
+
}
|
|
981
|
+
const item = normalizeChange({ target, change: args.change, why: args.why, verify: args.verify, status: allowed }, Date.now());
|
|
982
|
+
if (!item)
|
|
983
|
+
throw new Error("lume_change requires target and change");
|
|
984
|
+
await project.upsertChange(sid, item);
|
|
985
|
+
return { ok: true };
|
|
986
|
+
},
|
|
987
|
+
}));
|
|
988
|
+
ctx.tools.register(defineTool({
|
|
989
|
+
name: "lume_hypothesis",
|
|
990
|
+
description: "假设台账:记录一条正在验证的假设及其证据与状态(open/testing/confirmed/excluded)。排查类任务里每验证一次就更新状态;已排除的假设不要再重复尝试。",
|
|
991
|
+
parameters: {
|
|
992
|
+
text: { type: "string", required: true, description: "假设内容,一句话" },
|
|
993
|
+
evidence: { type: "string", description: "支持或推翻它的观察(含命令输出/时间戳摘要)" },
|
|
994
|
+
status: { type: "string", description: "open | testing | confirmed | excluded" },
|
|
995
|
+
},
|
|
996
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已更新假设台账" }] },
|
|
997
|
+
execute: async (args, exec) => {
|
|
998
|
+
if (!project)
|
|
999
|
+
throw new Error("lume project store is unavailable");
|
|
1000
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
1001
|
+
if (!sid)
|
|
1002
|
+
throw new Error("lume_hypothesis requires an active session");
|
|
1003
|
+
const item = normalizeHypothesis({ text: args.text, evidence: args.evidence, status: args.status }, Date.now());
|
|
1004
|
+
if (!item)
|
|
1005
|
+
throw new Error("lume_hypothesis requires text");
|
|
1006
|
+
await project.upsertHypothesis(sid, item);
|
|
1007
|
+
runtime.get(sid).hypothesesTouched = true;
|
|
1008
|
+
return { ok: true };
|
|
1009
|
+
},
|
|
1010
|
+
}));
|
|
1011
|
+
ctx.tools.register(defineTool({
|
|
1012
|
+
name: "lume_project_note",
|
|
1013
|
+
description: "记录一条**稳定的项目事实**(按工作目录跨会话累积):构建/测试命令、模块数据流、仓库约定、或一条死路(试过但行不通的做法)。只记可复用、已验证的事实,不要记一次性进展。",
|
|
1014
|
+
parameters: {
|
|
1015
|
+
kind: { type: "string", required: true, description: "build | test | module | convention | deadend" },
|
|
1016
|
+
text: { type: "string", required: true, description: "事实本身,一句话,≤200 字" },
|
|
1017
|
+
},
|
|
1018
|
+
output: { schema: OK_OUTPUT_SCHEMA, render: () => [{ type: "text", text: "已记入项目知识" }] },
|
|
1019
|
+
execute: async (args, exec) => {
|
|
1020
|
+
if (!project)
|
|
1021
|
+
throw new Error("lume project store is unavailable");
|
|
1022
|
+
const sid = String(exec?.agent?.session?.id ?? "");
|
|
1023
|
+
if (!sid)
|
|
1024
|
+
throw new Error("lume_project_note requires an active session");
|
|
1025
|
+
const fact = normalizeProjectFact({ kind: args.kind, text: args.text }, Date.now());
|
|
1026
|
+
if (!fact)
|
|
1027
|
+
throw new Error("lume_project_note requires text");
|
|
1028
|
+
await project.addFact(projectKeyFor(sid, { agent: exec?.agent }), fact, (candidate, existing) => existing.some((entry) => jaccard(entry.text, candidate) >= 0.7));
|
|
1029
|
+
return { ok: true };
|
|
1030
|
+
},
|
|
1031
|
+
}));
|
|
1032
|
+
}, "lume: carrier tools");
|
|
697
1033
|
}, "lume: persona tools");
|
|
698
1034
|
// ── 人设五段式注入 + 切换播报 ──
|
|
699
1035
|
/**
|
|
@@ -751,21 +1087,21 @@ export function apply(ctx, config = {}) {
|
|
|
751
1087
|
}).trim();
|
|
752
1088
|
// 易变的任务指令:路由、阶段、闲聊声明、长会话护栏、目标锚点、即时对齐、
|
|
753
1089
|
// 交付复核、压缩重锚、文档能力指引、失败纠偏、反思提醒——全部每步可变。
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
st.
|
|
761
|
-
st.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
st.
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1090
|
+
// 载具与方法块(契约/台账/假设/项目知识/影响面/文档方法/触发器提醒)排在最后:
|
|
1091
|
+
// 它们是「此刻最该看的」,紧贴尾部注意力最强位;超预算时先丢可丢块(composeBlocks)。
|
|
1092
|
+
const thinkingRuntime = composeBlocks([
|
|
1093
|
+
{ text: buildInteractionDirective(mode) },
|
|
1094
|
+
{ text: buildTaskPhaseDirective(st.taskPhase) },
|
|
1095
|
+
{ text: buildCasualDirective(TASK_SIGNAL_RE.test(query)) },
|
|
1096
|
+
{ text: buildLongSessionGuard(st.turnIndex) },
|
|
1097
|
+
{ text: buildSessionAnchor(st.turnIndex, mode, query, st.recentTurns) },
|
|
1098
|
+
{ text: st.alignmentCorrection },
|
|
1099
|
+
{ text: st.postTurnReview },
|
|
1100
|
+
{ text: st.compaction ? buildCompactionNotice(st.compaction, st.turnIndex) : null },
|
|
1101
|
+
{ text: st.protocolCorrection },
|
|
1102
|
+
{ text: reflectionStore?.getFeedback() ?? null, droppable: true },
|
|
1103
|
+
...carrierBlocks(sid, context, st, query, mode),
|
|
1104
|
+
]);
|
|
769
1105
|
// 会话选择尚未就绪(启动竞态):只出任务协议,人设段留空——与旧实现一致,
|
|
770
1106
|
// 也避免把「尚未选择」误记成一次人设切换。
|
|
771
1107
|
if (!currentStore)
|
|
@@ -853,16 +1189,49 @@ export function apply(ctx, config = {}) {
|
|
|
853
1189
|
get distill() {
|
|
854
1190
|
return distillRunner;
|
|
855
1191
|
},
|
|
1192
|
+
getProjectState(sessionId) {
|
|
1193
|
+
// 诊断视图:任务载具 + 项目知识(供排查"模型到底看到了什么")。
|
|
1194
|
+
if (!project)
|
|
1195
|
+
return null;
|
|
1196
|
+
const st = runtime.get(sessionId);
|
|
1197
|
+
return {
|
|
1198
|
+
projectKey: st.projectKey,
|
|
1199
|
+
contract: project.getContract(sessionId),
|
|
1200
|
+
changes: project.getChanges(sessionId),
|
|
1201
|
+
hypotheses: project.getHypotheses(sessionId),
|
|
1202
|
+
facts: st.projectKey ? project.getFacts(st.projectKey) : [],
|
|
1203
|
+
triggers: { ...st.triggerCounters, fired: st.triggerFiredAt },
|
|
1204
|
+
};
|
|
1205
|
+
},
|
|
1206
|
+
async clearProjectFacts(sessionId) {
|
|
1207
|
+
if (!project)
|
|
1208
|
+
return false;
|
|
1209
|
+
const st = runtime.get(sessionId);
|
|
1210
|
+
if (!st.projectKey)
|
|
1211
|
+
return false;
|
|
1212
|
+
await project.clearFacts(st.projectKey);
|
|
1213
|
+
return true;
|
|
1214
|
+
},
|
|
1215
|
+
});
|
|
1216
|
+
// ── RPC 通道 ──
|
|
1217
|
+
// 必须在**注入了 webServer 的作用域**里注册:新宿主的 `connection.rpc.handle` 内部会
|
|
1218
|
+
// 用调用方 ctx 执行 `owner.webServer.register(route)`(见 dsh-client-connection 的
|
|
1219
|
+
// `owner.effect(() => owner.webServer.register(route))`),缺注入时 cordis 直接抛
|
|
1220
|
+
// "cannot get property \"webServer\" without inject"。宿主自带的 dsh-ppt / dsh-api-gateway
|
|
1221
|
+
// 也都是 `ctx.inject(["webServer"], (webCtx) => webCtx.connection.rpc.handle(...))` 这个写法。
|
|
1222
|
+
// 用作用域注入而不是把 webServer 塞进顶层 inject:没有 web 载体的宿主(headless)里只
|
|
1223
|
+
// 失去 RPC 通道,插件其余功能照常工作,不会被 inject 卡住。
|
|
1224
|
+
ctx.inject(["webServer"], (webCtx) => {
|
|
1225
|
+
webCtx.effect(() => webCtx.connection.rpc.handle(LUME_CHANNEL, async (endpoint, payload) => {
|
|
1226
|
+
currentStore ??= await storesReady;
|
|
1227
|
+
identity ??= await identityReady;
|
|
1228
|
+
const result = await handleEndpoint(endpoint, payload);
|
|
1229
|
+
if (endpoint !== "list" && endpoint !== "getSessionPersona") {
|
|
1230
|
+
webCtx.logger?.warn?.(`lume: rpc ${endpoint} ${JSON.stringify(payload ?? {})} → ok=${result.ok}${result.ok ? "" : ` code=${result.error.code}`}`);
|
|
1231
|
+
}
|
|
1232
|
+
return result;
|
|
1233
|
+
}, { authority: "trusted-host" }), "lume: rpc channel");
|
|
856
1234
|
});
|
|
857
|
-
ctx.effect(() => ctx.connection.rpc.handle(LUME_CHANNEL, async (endpoint, payload) => {
|
|
858
|
-
currentStore ??= await storesReady;
|
|
859
|
-
identity ??= await identityReady;
|
|
860
|
-
const result = await handleEndpoint(endpoint, payload);
|
|
861
|
-
if (endpoint !== "list" && endpoint !== "getSessionPersona") {
|
|
862
|
-
ctx.logger?.warn?.(`lume: rpc ${endpoint} ${JSON.stringify(payload ?? {})} → ok=${result.ok}${result.ok ? "" : ` code=${result.error.code}`}`);
|
|
863
|
-
}
|
|
864
|
-
return result;
|
|
865
|
-
}, { authority: "trusted-host" }), "lume: rpc channel");
|
|
866
1235
|
// ── 系统提示词段落 ──
|
|
867
1236
|
ctx.effect(() => ctx.systemPrompt.section({
|
|
868
1237
|
name: LUME_PERSONA_SECTION,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lume-dsh-plugin",
|
|
3
|
-
"description": "微光 (Lume) — DSH Desktop
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "微光 (Lume) — DSH Desktop 增强插件:给会话装上工程纪律与真实关系。纪律层约束「如何正确完成任务」——意图路由、阶段门控、真实工具证据、交付前复核、文档能力感知;方法层把量化需求、改动台账、假设台账与项目知识变成可检查的产出,并用行为触发器在轨迹上纠偏(撒网不收敛 / 连写不验 / 死路重撞);人设层塑造「以何种风格表达」——从聊天记录、小说、剧本、设定文档蒸馏具名角色,长期记忆与风格随对话演进。约束按需注入,闲聊不额外付 token。",
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -56,7 +56,6 @@
|
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
58
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
59
|
-
"@deepseek-ai/dsh-client-ui-primitives": "*",
|
|
60
59
|
"@deepseek-ai/dsh-llm": "*",
|
|
61
60
|
"@deepseek-ai/dsh-storage-domain": "*",
|
|
62
61
|
"@deepseek-ai/dsh-tools": "*",
|
|
@@ -98,9 +97,6 @@
|
|
|
98
97
|
"@deepseek-ai/cordis": {
|
|
99
98
|
"optional": true
|
|
100
99
|
},
|
|
101
|
-
"@deepseek-ai/dsh-client-ui-primitives": {
|
|
102
|
-
"optional": true
|
|
103
|
-
},
|
|
104
100
|
"@deepseek-ai/dsh-llm": {
|
|
105
101
|
"optional": true
|
|
106
102
|
},
|