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.
Files changed (58) hide show
  1. package/CHANGELOG.md +460 -0
  2. package/README.md +245 -275
  3. package/lib/client.js +242 -181
  4. package/lib/core/card.js +11 -9
  5. package/lib/core/citations.js +235 -0
  6. package/lib/core/coverage.js +149 -0
  7. package/lib/core/dialogue-mining.js +49 -11
  8. package/lib/core/knowledge.js +165 -0
  9. package/lib/core/leak-detector.js +1 -3
  10. package/lib/core/ledger.js +129 -18
  11. package/lib/core/manifest.js +3 -1
  12. package/lib/core/memory-id.js +105 -0
  13. package/lib/core/metrics.js +270 -0
  14. package/lib/core/persona-limits.js +25 -0
  15. package/lib/core/scope.js +124 -0
  16. package/lib/core/signals.js +285 -5
  17. package/lib/core/task-memory.js +143 -0
  18. package/lib/core/text.js +45 -3
  19. package/lib/host/backfill.js +218 -0
  20. package/lib/host/bootstrap.js +130 -0
  21. package/lib/host/boundary.js +1 -3
  22. package/lib/host/clauses.js +180 -0
  23. package/lib/host/config.js +7 -0
  24. package/lib/host/diag.js +44 -7
  25. package/lib/host/distill-prompt.js +365 -0
  26. package/lib/host/distill.js +22 -348
  27. package/lib/host/extraction.js +11 -3
  28. package/lib/host/host-context.js +1 -0
  29. package/lib/host/host-events.js +100 -0
  30. package/lib/host/identity.js +12 -29
  31. package/lib/host/inbound.js +133 -0
  32. package/lib/host/injection.js +2 -6
  33. package/lib/host/llm-aux.js +130 -0
  34. package/lib/host/llm-route.js +3 -0
  35. package/lib/host/methods.js +182 -11
  36. package/lib/host/metrics-log.js +169 -0
  37. package/lib/host/notices.js +62 -0
  38. package/lib/host/project-access.js +210 -0
  39. package/lib/host/project.js +153 -2
  40. package/lib/host/prompt-blocks.js +113 -0
  41. package/lib/host/protocol.js +144 -11
  42. package/lib/host/reflection.js +28 -5
  43. package/lib/host/registry.js +0 -4
  44. package/lib/host/requirements-scan.js +108 -0
  45. package/lib/host/rpc-bridge.js +23 -4
  46. package/lib/host/rpc.js +1 -1
  47. package/lib/host/sections.js +48 -0
  48. package/lib/host/session-deps.js +27 -0
  49. package/lib/host/session-events.js +371 -0
  50. package/lib/host/session-runtime.js +18 -5
  51. package/lib/host/thinking.js +10 -1
  52. package/lib/host/tools.js +367 -0
  53. package/lib/host/triggers.js +30 -6
  54. package/lib/host/turn-boundary.js +116 -0
  55. package/lib/host/wiring.js +280 -0
  56. package/lib/host/workspace-map.js +81 -0
  57. package/lib/index.js +334 -836
  58. package/package.json +13 -4
@@ -0,0 +1,113 @@
1
+ import { noticeOpen, noticeText, setNotice } from "./notices.js";
2
+ import * as taskMemoryMod from "../core/task-memory.js";
3
+ /**
4
+ * 当前步的易变段:路由 / 阶段 / 闲聊声明 / 长会话护栏 / 目标锚点 / 对齐 / 交付复核 /
5
+ * 压缩重锚 / 协议纠偏 / 反思提醒 + 载具与方法块。
6
+ *
7
+ * 顺序有讲究:把「此刻最该做的一件事」(触发器提醒)放在最后——尾部注意力最强。
8
+ * 超预算时先丢 `droppable` 块(见 methods.composeBlocks)。
9
+ */
10
+ export function volatileBlocks(deps, input) {
11
+ const { st, query, mode } = input;
12
+ return [
13
+ { text: deps.buildInteractionDirective(mode) },
14
+ { text: deps.buildTaskPhaseDirective(st.taskPhase) },
15
+ // 条款预算:完整协议在系统提示里一条不少,这里只按本轮形态加权最相关的三条
16
+ { text: deps.buildFocusClauseDirective(input) },
17
+ { text: deps.buildCasualDirective(deps.taskSignalRe.test(query)) },
18
+ { text: deps.buildLongSessionGuard(st.turnIndex) },
19
+ { text: deps.buildSessionAnchor(st.turnIndex, mode, query, st.recentTurns) },
20
+ { text: noticeText(st, "align") },
21
+ { text: noticeText(st, "postTurn") },
22
+ { text: st.compaction ? deps.buildCompactionNotice(st.compaction, st.turnIndex) : null },
23
+ { text: noticeText(st, "protocol") },
24
+ { text: deps.reflectionFeedback(), droppable: true },
25
+ ...carrierBlocks(deps, input),
26
+ ];
27
+ }
28
+ /**
29
+ * 任务载具块:契约 / 改动台账 / 假设台账 / 设计决策 / 需求锚点 / 项目知识 + 方法块 + 触发器提醒。
30
+ *
31
+ * 全部落在尾部快照(易变层)——这正是「载具」现在才做得起的理由:0.6.2 之前每步注入一份
32
+ * 会变的状态等于每步作废整段前缀,而快照只在内容变化时才付费(实测 58 步只产生 9 条快照)。
33
+ */
34
+ export function carrierBlocks(deps, input) {
35
+ if (!deps.projectMemoryOn)
36
+ return [];
37
+ const { sid, context, st, query, mode } = input;
38
+ // 第一轮装配时 cwd 还没到(宿主的运行时快照**晚于**系统提示装配,差 1 毫秒)→
39
+ // 用「会话目录名 → 工作目录」映射提前补上;否则依赖 cwd 的〔项目知识〕在**新会话第一轮**缺席
40
+ // (现场 14:15:模型答“我这轮没接上上下文,把方案文件发我”,而库里明明有 40 条知识)。
41
+ deps.ensureSessionWorkspace(sid, st);
42
+ const isTask = mode !== "question" || deps.taskSignalRe.test(query);
43
+ // 方法块只在**已定路由不是问答**时出现:路由说"直接回答、别调工具",方法块说"先写契约",
44
+ // 两者同时出现时模型只能赌(实测 13 轮 0 次契约调用)。问答轮保留事实回显(锚点/台账/知识)。
45
+ const taskMethods = mode !== "question";
46
+ // 契约是"动手前的产出":讨论轮问的是取舍,塞"开工前先写任务契约"只会稀释它(实测噪音)。
47
+ const contractMethods = mode === "execute" || mode === "diagnosis";
48
+ const contract = deps.contractOf(sid);
49
+ const changes = deps.changesOf(sid);
50
+ const docDirective = deps.documentDirective(query, context);
51
+ // 需求覆盖核对:文档产物一写出来,就把「需求原句」与「交付物里的句子」并列,
52
+ // 替代模型的自证式「N 条全有落点」(2026-09-23 现场:自证全绿,实际藏着三处硬伤)。
53
+ if (!noticeText(st, "coverage") && noticeOpen(st, "coverage") && st.agent.artifactText.length > 200) {
54
+ // 只对着**需求原文**做覆盖核对:表里混着闲聊与评审粘贴,挑不出来就整段跳过(宁可不做也不做错)
55
+ const requirementText = deps.pickRequirementCorpus(deps.requirementsOf(sid));
56
+ const items = deps.splitRequirementItems(requirementText);
57
+ if (items.length >= 2) {
58
+ setNotice(st, "coverage", deps.buildRequirementCoverageDirective(deps.coverageRows(items, st.agent.artifactText), {
59
+ figures: deps.hasFigureRefs(requirementText) && !deps.hasFigureRefs(st.agent.artifactText),
60
+ danglingRefs: deps.danglingSectionRefs(st.assistantText, st.agent.artifactText),
61
+ }));
62
+ }
63
+ }
64
+ return [
65
+ // 契约:有就回显(交付轮切成对账口径),没有且是任务轮就先教它写一份。
66
+ { text: deps.renderContract(contract, st.taskPhase === "deliver") },
67
+ { text: !contract && contractMethods ? deps.buildContractMethodDirective() : null, droppable: true },
68
+ // 设计三问:设计型任务且还没写下设计时反复顶(实测一次提示会被忽略)
69
+ { text: deps.needsDesignPass(sid, st, query, mode) ? deps.buildDesignMethodDirective() : null, droppable: true },
70
+ // 需求解读三条硬规则:用户刚给/改了需求时顶
71
+ { text: st.requirementFresh ? deps.buildRequirementMethodDirective(taskMethods) : null, droppable: true },
72
+ // 台账与假设:存在就回显——让模型「看见」自己的计划,而不是记在脑子里。
73
+ { text: changes.length > 0 ? deps.renderChangeLedger(changes) : null },
74
+ { text: deps.renderHypotheses(deps.hypothesesOf(sid)) },
75
+ // 设计决策:跨轮/跨压缩回显,让「数据落在哪 / 接口 / 范式 / 取舍」不随上下文漂移
76
+ { text: deps.renderDesign(deps.designOf(sid)) },
77
+ // 需求锚点:逐字回显用户原话(非可丢块——它是最不该漂移的东西)
78
+ { text: deps.renderRequirements(deps.requirementsOf(sid)) },
79
+ // 需求覆盖核对:需求原句 vs 交付物句子(只在有文档产物时出现)
80
+ { text: noticeText(st, "coverage") },
81
+ // 会话记忆:只在**冷启动**(没有契约/台账/需求锚点)时顶上来。
82
+ // 这是「换个窗口接着干」的入口:上下文撑满时宿主压缩会失败,那个会话再也聊不动,
83
+ // 所以新会话必须能一眼看到上次的目标、已拍板、未决与关键定位。
84
+ {
85
+ text: deps.isColdStart({
86
+ hasContract: Boolean(deps.contractOf(sid)),
87
+ changes: deps.changesOf(sid).length,
88
+ requirements: deps.requirementsOf(sid).length,
89
+ })
90
+ ? deps.renderTaskMemory(st.taskMemories?.[0] ?? null, {
91
+ recent: (st.taskMemories ?? [])
92
+ .slice(0, 4)
93
+ .map((item) => ({ title: String(item.title ?? ""), at: Number(item.at ?? 0) })),
94
+ })
95
+ : null,
96
+ droppable: true,
97
+ },
98
+ // 项目知识:**任何轮次都渲染**——它是事实(跨会话累积的约定/命令/死路),不是方法指引。
99
+ // 现场教训:曾只在非问答轮渲染,于是「新开会话先问一句『你知道 X 需求吗』」这种最自然的开场白
100
+ // 恰好看不到知识,用户会以为沉淀没生效。事实类回显与需求锚点同等处理(可丢块,成本可控)。
101
+ { text: deps.renderProjectFacts(deps.factsOf(sid, context), 14, st.sessionTitle), droppable: true },
102
+ // 方法块:按任务形态出现;文档方法论只在判定为文档任务时出现。
103
+ { text: isTask && mode !== "question" ? deps.buildImpactDirective() : null, droppable: true },
104
+ { text: docDirective ? deps.buildDocumentMethodDirective() : null },
105
+ { text: mode === "execute" || mode === "diagnosis" ? deps.buildStructureHint(deps.structureToolName(context)) : null, droppable: true },
106
+ { text: noticeText(st, "turn") },
107
+ { text: noticeText(st, "trigger") },
108
+ { text: noticeText(st, "drift") },
109
+ { text: noticeText(st, "citation") },
110
+ { text: noticeText(st, "claim") },
111
+ { text: noticeText(st, "question") },
112
+ ];
113
+ }
@@ -1,6 +1,6 @@
1
1
  const MODE_RULES = {
2
- question: "当前模式:问答。先直接回答问题;不要擅自修改文件、调用工具或替用户做决定。",
3
- research: "当前模式:查找。先收集并区分已知、未知和推断;未经明确授权不要修改外部状态。",
2
+ question: "当前模式:问答。先直接回答;**只读核实该做就做**(read / grep / glob / git log 这类只读工具允许用)——答案依赖仓库事实时,不许用「我不确定」「这是新会话」作答;但不要改动文件、不要替用户做决定。",
3
+ research: "当前模式:查找。先收集并区分已知、未知和推断;**该查就查**(只读工具随便用),但未经明确授权不要修改外部状态(文件 / 提交 / 远端 / 数据库)。",
4
4
  discussion: "当前模式:讨论。先比较选项、取舍和风险;不要把探讨中的方案当成已决定的执行方案。",
5
5
  diagnosis: "当前模式:诊断。先说明现象、证据、可能根因和验证办法;除非用户明确要求修复,不越权修复,不要越过诊断边界动手。",
6
6
  execute: "当前模式:执行。先确认目标和完成标准,再做最小变更;交付时明确列出“已完成、已验证、未验证、残留副作用”,不要用动作完成冒充目标达成。",
@@ -8,6 +8,12 @@ const MODE_RULES = {
8
8
  // 显式请求标记后必须紧跟一个动作动词,且限制在同一小句内(旧版用 `.*` 贪婪跨越
9
9
  // 整句,导致「是什么驱动你去这么做的」也被判成执行)。句首祈使不再放行「做」:
10
10
  // 「做一件事…」这类名词化表述是讨论而非执行。
11
+ /**
12
+ * 设计信号(0.7.5):命中说明这条需求要动**数据或接口**,值得走一次设计 pass。
13
+ * 现场样本(B2I 优惠视图与订单属性):新增字段 + 模糊搜索 + 分页改造 + 历史数据刷 + 外部同步,
14
+ * 全是设计型动作,但会话里没有任何设计决策。这条正则只用于「该不该顶设计三问」,不参与模式路由。
15
+ */
16
+ export const DESIGN_SIGNAL_RE = /新增|添加|加个|加一个|字段|属性|接口|表结构|建表|改表|页面|分页|导出|导入|批量|视图|列表|搜索|筛选|下拉|同步|对接|联调|迁移|兼容|需求|功能|模块|组件|设计/;
11
17
  const EXECUTE_RE = new RegExp([
12
18
  "(?:请|帮我|帮忙|直接|把|给我|替我|麻烦|需要你)\\s*[^,。!?;\\n]{0,24}?(?:做|改|修|写|加|删|建|跑|执行|完成|实现|优化|更新|部署|安装|迁移|提交|发布|检查|核对|补|替换|重命名|合并|回滚|加上)",
13
19
  "^(?:改|修|写|加|删|建|跑|执行|完成|实现|优化|更新|部署|安装|迁移|提交|发布|检查|补|替换|重命名|合并|回滚)",
@@ -19,19 +25,138 @@ const RESEARCH_RE = /查一下|查找|搜索|检索|资料|文档|来源|证据|
19
25
  /**
20
26
  * 采用“明确执行 > 诊断 > 讨论 > 查找 > 问答”的优先级,避免把“为什么”误判成修复命令。
21
27
  */
22
- export function classifyInteraction(text) {
28
+ /**
29
+ * 执行动词的补充表(0.7.4 起,0.8.x 扩表)。
30
+ *
31
+ * 现场实测:`EXECUTE_RE` 不含「重构 / 梳理 / 删掉 / 合并 / 迁移」等常见动词,于是
32
+ * 「帮我重构订单退费链路」被判成**问答**——方法层(影响面清单)与阶段门控都不生效,
33
+ * 而那次会话确实改了 11 个文件。这里只补「对产物动手」的动作,**不含诊断类动词**
34
+ * (分析/诊断/排查属于 diagnosis,混进来会把讨论与排查误判成执行)。
35
+ *
36
+ * 扩表理由(2026-09-24 复盘):词表永远补不完(「把这部分整理一下」曾整句落空),
37
+ * 所以本表只当**第一层证据**,真正的兜底是 classifyWithTrajectory 的轨迹证据。
38
+ */
39
+ const EXECUTE_EXTRA_RE = /重构|重写|梳理|清理|删掉|删除|去掉|移除|合并|合入|迁移|替换|收口|落地|接入|适配|升级|降级|补上|补齐|加上|改成|改为|换成|拆开|拆出|整理|归置|收拾|精简|瘦身|补全|抽出来|挪到|调整一下|改一下|优化一下|重排/;
40
+ /**
41
+ * 「在问怎么做 / 能不能做」的语用标记:命中说明这一句要的是**判断与办法**,不是让人动手。
42
+ * 为什么必须有这道闸(2026-09-24 复盘):动词表与语用层是两件事——「怎么整理这段数据比较好」
43
+ * 含执行动词「整理」,但它要的是方案;旧版靠动词命中即判执行,于是讨论被当成命令。
44
+ */
45
+ const HOWTO_RE = /^\s*(怎么|怎样|如何|为什么|为啥|是否|能否|能不能|可不可以|该不该|要不要)|比较好|更好一点|哪种更/;
46
+ /** 变更对象:软性请求(能不能/可否)必须真的指向一个可改动的东西,否则只是普通疑问句。 */
47
+ const CHANGE_OBJECT_RE = /优化|重构|重写|整理|清理|拆分|合并|迁移|替换|升级|调整|改进|简化|精简|改造|重排|补全/;
48
+ /** 能力/可行性询问:「看看这块能不能优化」→ 先给判断与办法(诊断),不是让人直接动手。 */
49
+ const SOFT_CHANGE_RE = /能不能|能否|可不可以|是否可以|有没有办法|有没有可能|可否/;
50
+ /**
51
+ * 复核既有改动:再/重新/再次 + 「看/查/评价」一族动词 + 代码/改动/实现/方案。
52
+ *
53
+ * 覆盖要够宽(二审指出:原来只枚举六个动词,「再次评价一下刚才的改动」就漏了,靠默认落回问答蒙对):
54
+ * 检查/核对/检验/验证/确认/评价/评估/评审/审阅/审一遍/过一遍/看一遍/看看/看一下 都算;前缀可省。
55
+ */
56
+ const RECHECK_RE = /(?:(?:再|重新|从头|再次|复核|复检|复审|复查|回顾)[^。;\n]{0,8})?(?:检查|核对|检验|验证|确认|评价|评估|评审|审阅|审一遍|审一下|过一遍|看一遍|看看|看一下)[^。;\n]{0,12}(?:代码|改动|修改|实现|产物|结果|逻辑|方案|思路)/;
57
+ /**
58
+ * 路由纠正语用:用户在说「你刚才理解错了这一轮要什么」。
59
+ * 与 session-events 的通用纠偏词分开——那条管语气与表达,这条只管**模式判错**,
60
+ * 因为它要做的事不同:拿被纠正前的那句话重算路由(见 classifyWithTrajectory)。
61
+ */
62
+ export const ROUTE_CORRECTION_RE = /不是让你|我没让你|谁说让你|我只是问|我只是想|我问的是|我是问|不是要你改|不是让你改|你理解错|答非所问|我说的是|我指的是|先别改|别改|不要动|你改错|搞错方向/;
63
+ /**
64
+ * 当前句自带「新问题」特征:疑问代词/疑问语气/显式换话题。
65
+ *
66
+ * 为什么需要(2026-09-24 审核指出):中文常不带问号,轨迹抬档只看「非 ? 结尾 + ≤120 字 + 近三轮任务型」,
67
+ * 于是「顺便说下 git 怎么配」会被抬成执行。带上这条反向条件:明显在问,就一律不抬档——
68
+ * 宁可少一次加权,也不要把「问一句」判成「动手」。
69
+ */
70
+ const NEW_QUESTION_RE = /怎么|如何|为什么|为啥|什么|哪些|哪个|是否|吗\s*$|呢\s*$|换个话题|另一个问题|另外问|顺便问|新问题/;
71
+ /** 承接式追问:没有新动词,但明显在上一件事上往下走。 */
72
+ const FOLLOW_UP_RE = /^\s*(继续|接着|然后|还有|再|那|所以|顺便|下一步|再来|往下|then|接着来|继续吧|那这个|这样的话|那它)/i;
73
+ /**
74
+ * 采用“明确执行 > 诊断 > 讨论 > 查找 > 问答”的优先级,避免把“为什么”误判成修复命令。
75
+ *
76
+ * 返回值带命中判据与来源(0.8.x):路由是**唯一一处「判错就全盘错」**的决策,
77
+ * 而它此前不可观测——判错了没人知道,也没法统计哪条正则在误判。详细版让每次判定
78
+ * 都能落进度量(见 core/metrics.ts 与 tools 的 lume_metrics)。
79
+ */
80
+ export function classifyInteractionDetailed(text) {
23
81
  const query = String(text ?? "").trim();
24
82
  if (!query)
25
- return "question";
26
- if (EXECUTE_RE.test(query))
27
- return "execute";
83
+ return { mode: "question", matched: "empty", source: "text" };
84
+ const howto = HOWTO_RE.test(query);
85
+ // 复核既有改动要**先于**执行判定:这类句子的宾语本身就是「改动/修改/实现」,
86
+ // 若让动词表先跑,「帮我看一遍刚才的修改」会被抢成 execute(二审的同一个洞)。
87
+ // 但**明确命令优先**:宾语里有动作(改成/删掉/替换/重写…)就还是执行——
88
+ // 「检查一下这段代码,有问题就改成对的」要的是动手,不是诊断(二审指出的代价)。
89
+ if (!howto && !EXECUTE_EXTRA_RE.test(query) && RECHECK_RE.test(query))
90
+ return { mode: "diagnosis", matched: "recheck", source: "text" };
91
+ if (!howto && EXECUTE_RE.test(query))
92
+ return { mode: "execute", matched: "execute-request", source: "text" };
93
+ if (!howto && EXECUTE_EXTRA_RE.test(query))
94
+ return { mode: "execute", matched: "execute-verb", source: "text" };
95
+ // 可行性询问放在执行之后:显式命令(把 X 改一下)仍然优先判执行,
96
+ // 只有「没下命令、只在问能不能」的句子才落到诊断——诊断模式不改文件,但会给办法。
97
+ if (!howto && SOFT_CHANGE_RE.test(query) && CHANGE_OBJECT_RE.test(query))
98
+ return { mode: "diagnosis", matched: "capability-ask", source: "text" };
99
+ // 复核既有改动(真机实测:「做了修改,你再重新检查一下代码」被判成 research):
100
+ // 「再/重新 + 检查/复核 + 代码/改动」要的是判断与结论,不是检索资料 → 诊断。
28
101
  if (DIAGNOSIS_RE.test(query))
29
- return "diagnosis";
102
+ return { mode: "diagnosis", matched: "diagnosis", source: "text" };
30
103
  if (DISCUSSION_RE.test(query))
31
- return "discussion";
104
+ return { mode: "discussion", matched: "discussion", source: "text" };
32
105
  if (RESEARCH_RE.test(query))
33
- return "research";
34
- return "question";
106
+ return { mode: "research", matched: "research", source: "text" };
107
+ return { mode: "question", matched: "fallback", source: "text" };
108
+ }
109
+ export function classifyInteraction(text) {
110
+ return classifyInteractionDetailed(text).mode;
111
+ }
112
+ /**
113
+ * 轨迹路由(0.8.x):单句正则只看「这一句话的词」,中文是发散的——「把这部分整理一下」
114
+ * 里换了动词就落空,而**判错的代价是后面全部条款都指向错的方向**(现场:执行轮被当成
115
+ * 问答,方法层与阶段门控整轮不生效,而那次会话改了 11 个文件)。
116
+ *
117
+ * 这里补的不是更多词,是三类**轨迹证据**:
118
+ * ① 纠正:用户说「不是让你改 / 我问的是」→ 拿被纠正前那句重算,而不是在纠正句上再猜一次;
119
+ * ② 粘性:在途任务(阶段已推进或已动过文件)上的承接式追问 → 保持任务模式,
120
+ * 不因为「那继续吧」这种没有动词的句子掉回问答;
121
+ * ③ 一致:最近几轮用户都在做任务、当前这句又没有疑问特征 → 按轨迹取任务模式。
122
+ * 证据不足时**一律回落到单句结果**——轨迹只用来补,不用来猜。
123
+ */
124
+ export function classifyWithTrajectory(input) {
125
+ const base = classifyInteractionDetailed(input.text);
126
+ const query = String(input.text ?? "").trim();
127
+ const recent = input.recentUserTexts ?? [];
128
+ const correction = input.correction ?? ROUTE_CORRECTION_RE.test(query);
129
+ // ① 纠正:从最近往前找第一句「本来是任务」的发言,用它重算。
130
+ if (correction) {
131
+ for (let i = recent.length - 1; i >= 0; i--) {
132
+ const prior = classifyInteractionDetailed(recent[i]);
133
+ if (prior.mode === "execute" || prior.mode === "diagnosis")
134
+ return { mode: prior.mode, matched: prior.matched, source: "correction", evidence: recent[i] };
135
+ }
136
+ return { mode: base.mode, matched: base.matched, source: "correction", evidence: query };
137
+ }
138
+ const followUp = FOLLOW_UP_RE.test(query) && query.length <= 120;
139
+ // 自带疑问特征 → 不参与抬档(粘性也一样:追问可以,问新问题不行)
140
+ const asksNewQuestion = NEW_QUESTION_RE.test(query);
141
+ // ② 粘性:任务在途 + 承接式追问 + 单句判成问答(明显掉档)→ 保持上一轮模式。
142
+ // 只认「阶段已推进 / 已动过文件」的在途任务:否则一次误判会被无限继承。
143
+ const inFlight = input.hadMutations || input.prevPhase === "execute" || input.prevPhase === "verify" || input.prevPhase === "deliver";
144
+ if (followUp && !asksNewQuestion && base.mode === "question" && inFlight && input.prevMode !== "question")
145
+ return { mode: input.prevMode, matched: `sticky:${base.matched}`, source: "sticky", evidence: query };
146
+ // ③ 一致:最近三轮用户话里至少两轮是任务型,当前这句既没有疑问特征也不是新话题 → 按轨迹判任务。
147
+ if (base.mode === "question" && !followUp && !asksNewQuestion && query.length <= 120 && !/[??]\s*$/.test(query)) {
148
+ const taskish = recent
149
+ .slice(-3)
150
+ .map((turn) => classifyInteractionDetailed(turn))
151
+ .filter((decision) => decision.mode === "execute" || decision.mode === "diagnosis");
152
+ if (taskish.length >= 2) {
153
+ // 延续**最近一轮**的任务模式:比「按词猜这一句是要分析还是要动手」可靠——
154
+ // 轨迹是证据,词表只是提示。判错也只是少一次加权,不会白动手(执行前仍有阶段门控)。
155
+ const mode = taskish[taskish.length - 1].mode;
156
+ return { mode, matched: `trajectory:${base.matched}`, source: "trajectory", evidence: query };
157
+ }
158
+ }
159
+ return base;
35
160
  }
36
161
  /**
37
162
  * 判定一条消息是否出自真实用户。
@@ -65,7 +190,15 @@ export function buildCasualDirective(isTask) {
65
190
  return "〔本轮类型〕闲聊轮:协议里的任务条款(任务分解、阶段门控、验证清单、交付复核)本轮不适用——直接回答,不要输出执行计划,也不要为简单问题增加调研与验证步骤。";
66
191
  }
67
192
  export function taskPhaseForMode(mode) {
68
- return mode === "research" ? "research" : mode === "discussion" ? "discuss" : mode === "diagnosis" ? "diagnose" : mode === "execute" ? "execute" : "answer";
193
+ return mode === "research"
194
+ ? "research"
195
+ : mode === "discussion"
196
+ ? "discuss"
197
+ : mode === "diagnosis"
198
+ ? "diagnose"
199
+ : mode === "execute"
200
+ ? "execute"
201
+ : "answer";
69
202
  }
70
203
  /**
71
204
  * 阶段只前进,不回退到初始的「回答」。
@@ -8,15 +8,30 @@
8
8
  import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
9
9
  import z from "@deepseek-ai/schemastery";
10
10
  import { zodLike } from "./identity.js";
11
- import { extractBalancedAt } from "./distill.js";
11
+ import { extractBalancedAt } from "./distill-prompt.js";
12
12
  export const LUME_REFLECTION_SPEC = defineDomain({
13
13
  name: "lume_reflection",
14
14
  version: 1,
15
15
  tables: {
16
16
  logs: domainTable(zodLike(z.union([
17
- z.object({ at: z.number(), context: z.number(), planning: z.number(), verification: z.number(), review: z.number(), diagnosis: z.number(), note: z.string() }),
17
+ z.object({
18
+ at: z.number(),
19
+ context: z.number(),
20
+ planning: z.number(),
21
+ verification: z.number(),
22
+ review: z.number(),
23
+ diagnosis: z.number(),
24
+ note: z.string(),
25
+ }),
18
26
  // v0.7.0 之前的四维日志
19
- z.object({ at: z.number(), context: z.number(), planning: z.number(), verification: z.number(), review: z.number(), note: z.string() }),
27
+ z.object({
28
+ at: z.number(),
29
+ context: z.number(),
30
+ planning: z.number(),
31
+ verification: z.number(),
32
+ review: z.number(),
33
+ note: z.string(),
34
+ }),
20
35
  // v0.4.0 之前的存量日志;仅用于打开域并在启动时迁移。
21
36
  z.object({ at: z.number(), p0: z.number(), p1: z.number(), p2: z.number(), p3: z.number(), note: z.string() }),
22
37
  ]))),
@@ -69,7 +84,11 @@ export class ReflectionStore {
69
84
  return value;
70
85
  }
71
86
  #computeFeedback() {
72
- const entries = [...this.#table.keys()].map((key) => this.#table.get(key)).filter((e) => typeof e?.context === "number").sort((a, b) => b.at - a.at).slice(0, 5);
87
+ const entries = [...this.#table.keys()]
88
+ .map((key) => this.#table.get(key))
89
+ .filter((e) => typeof e?.context === "number")
90
+ .sort((a, b) => b.at - a.at)
91
+ .slice(0, 5);
73
92
  if (entries.length < 3)
74
93
  return null;
75
94
  const dims = ["context", "planning", "verification", "review", "diagnosis"];
@@ -117,7 +136,11 @@ export function buildReflectionPrompt(turns) {
117
136
  };
118
137
  }
119
138
  export function parseReflectionScore(output) {
120
- const trimmed = output.trim().replace(/^```(?:json)?/i, "").replace(/```$/, "").trim();
139
+ const trimmed = output
140
+ .trim()
141
+ .replace(/^```(?:json)?/i, "")
142
+ .replace(/```$/, "")
143
+ .trim();
121
144
  let parsed;
122
145
  try {
123
146
  parsed = JSON.parse(trimmed);
@@ -56,7 +56,3 @@ export class PersonaRegistry {
56
56
  return out;
57
57
  }
58
58
  }
59
- /** 自定义人设记录 → Persona 形状的转换(供工具/RPC 校验复用)。 */
60
- export function customToRecord(name, custom) {
61
- return { name, displayName: custom.displayName, description: custom.description, promptText: custom.promptText, corpus: [] };
62
- }
@@ -0,0 +1,108 @@
1
+ import { readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ const MAX_DOC_FILES = 12;
4
+ const MAX_KEYWORDS = 60;
5
+ /** 只认"像项目标识符"的大写词:含下划线且够长,或纯大写且 ≥8 字(表名/常量/字段名)。 */
6
+ const IDENTIFIER_RE = /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b|\b[A-Z][A-Z0-9_]{7,}\b/g;
7
+ /** 常见技术词:不是"某需求的标识符",命中它们会把知识误归到别的需求。 */
8
+ const IDENTIFIER_STOPLIST = new Set([
9
+ "POSTGRESQL",
10
+ "MYSQL",
11
+ "ORACLE",
12
+ "MARIADB",
13
+ "SQLSERVER",
14
+ "JAVASCRIPT",
15
+ "TYPESCRIPT",
16
+ "HTTP",
17
+ "HTTPS",
18
+ "JSON",
19
+ "XML",
20
+ "YAML",
21
+ "UUID",
22
+ "URL",
23
+ "URI",
24
+ "API",
25
+ "APIS",
26
+ "DDL",
27
+ "DML",
28
+ "SQL",
29
+ "HTML",
30
+ "CSS",
31
+ "README",
32
+ "TODO",
33
+ "FIXME",
34
+ "NULL",
35
+ "TRUE",
36
+ "FALSE",
37
+ "SELECT",
38
+ "INSERT",
39
+ "UPDATE",
40
+ "DELETE",
41
+ "WHERE",
42
+ ]);
43
+ /** 不是需求目录的常见子目录(`doc/` 下混着这些)。 */
44
+ const NON_REQUIREMENT_DIRS = new Set(["GOOSE", "记忆", "项目理解", "需求模板", "模板", "参考", "归档", "TEMP", "_LUME", "DOCS", "ASSETS"]);
45
+ function looksLikeIdentifier(token) {
46
+ if (IDENTIFIER_STOPLIST.has(token))
47
+ return false;
48
+ if (/^\d+$/.test(token))
49
+ return false;
50
+ return token.includes("_") || token.length >= 8;
51
+ }
52
+ const cache = new Map();
53
+ function collectKeywords(dir) {
54
+ const found = new Set();
55
+ let entries = [];
56
+ try {
57
+ entries = readdirSync(dir, { withFileTypes: true })
58
+ .filter((entry) => entry.isFile() && /\.(md|sql|txt|json)$/i.test(entry.name))
59
+ .map((entry) => entry.name)
60
+ .slice(0, MAX_DOC_FILES);
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ for (const name of entries) {
66
+ try {
67
+ const text = readFileSync(join(dir, name), "utf8");
68
+ for (const hit of text.matchAll(IDENTIFIER_RE))
69
+ if (looksLikeIdentifier(hit[0]))
70
+ found.add(hit[0]);
71
+ if (found.size >= MAX_KEYWORDS)
72
+ break;
73
+ }
74
+ catch {
75
+ /* 单个文件读不了就跳过 */
76
+ }
77
+ }
78
+ return [...found].slice(0, MAX_KEYWORDS);
79
+ }
80
+ export function requirementHintsOf(cwd) {
81
+ const key = String(cwd ?? "").trim();
82
+ if (!key)
83
+ return [];
84
+ const cached = cache.get(key);
85
+ if (cached)
86
+ return cached;
87
+ let hints = [];
88
+ try {
89
+ const docRoot = join(key, "doc");
90
+ hints = readdirSync(docRoot, { withFileTypes: true })
91
+ .filter((entry) => entry.isDirectory() &&
92
+ entry.name.length > 0 &&
93
+ entry.name.length <= 60 &&
94
+ !entry.name.startsWith("_") &&
95
+ !entry.name.startsWith(".") &&
96
+ !NON_REQUIREMENT_DIRS.has(entry.name.toUpperCase()))
97
+ .map((entry) => ({ name: entry.name, keywords: collectKeywords(join(docRoot, entry.name)) }));
98
+ }
99
+ catch {
100
+ hints = []; // 没有 doc 目录:退化成"没有线索",判定回到会话标题
101
+ }
102
+ cache.set(key, hints);
103
+ return hints;
104
+ }
105
+ /** 测试/扫描用:清缓存。 */
106
+ export function clearRequirementHintsCache() {
107
+ cache.clear();
108
+ }
@@ -61,12 +61,31 @@ export function parseClientRequest(channel, request) {
61
61
  return { kind: "respond", status: 400, body: "body is not JSON", contentType: "text/plain" };
62
62
  }
63
63
  const envelope = body;
64
- if (!envelope || typeof envelope !== "object" || envelope.type !== "client-request" || typeof envelope.rpcId !== "string" || typeof envelope.method !== "string") {
65
- const rpcId = typeof envelope?.rpcId === "string" ? String(envelope.rpcId) : "invalid-request";
66
- return { kind: "respond", status: 200, body: connectionResponse(rpcId, { ok: false, error: { code: "gateway/bad-request", message: "invalid client-request message" } }), contentType: "application/json" };
64
+ if (!envelope ||
65
+ typeof envelope !== "object" ||
66
+ envelope.type !== "client-request" ||
67
+ typeof envelope.rpcId !== "string" ||
68
+ typeof envelope.method !== "string") {
69
+ const rpcId = typeof envelope?.rpcId === "string"
70
+ ? String(envelope.rpcId)
71
+ : "invalid-request";
72
+ return {
73
+ kind: "respond",
74
+ status: 200,
75
+ body: connectionResponse(rpcId, { ok: false, error: { code: "gateway/bad-request", message: "invalid client-request message" } }),
76
+ contentType: "application/json",
77
+ };
67
78
  }
68
79
  if (envelope.method !== endpoint) {
69
- return { kind: "respond", status: 200, body: connectionResponse(envelope.rpcId, { ok: false, error: { code: "gateway/bad-request", message: `endpoint mismatch: ${envelope.method} vs ${endpoint}` } }), contentType: "application/json" };
80
+ return {
81
+ kind: "respond",
82
+ status: 200,
83
+ body: connectionResponse(envelope.rpcId, {
84
+ ok: false,
85
+ error: { code: "gateway/bad-request", message: `endpoint mismatch: ${envelope.method} vs ${endpoint}` },
86
+ }),
87
+ contentType: "application/json",
88
+ };
70
89
  }
71
90
  return { kind: "dispatch", rpcId: envelope.rpcId, endpoint, payload: envelope.payload };
72
91
  }
package/lib/host/rpc.js CHANGED
@@ -1,4 +1,4 @@
1
- import { settleMemoryText, STORY_MEMORY_CAP } from "./distill.js";
1
+ import { settleMemoryText, STORY_MEMORY_CAP } from "./distill-prompt.js";
2
2
  import { normalizeCard, parseCard } from "../core/card.js";
3
3
  import { isCoreMemory } from "./injection.js";
4
4
  function requireString(payload, field) {
@@ -0,0 +1,48 @@
1
+ export function installPromptSections(deps) {
2
+ const { ctx } = deps;
3
+ ctx.effect(() => ctx.systemPrompt.section({
4
+ name: deps.personaSection,
5
+ order: deps.personaOrder,
6
+ text: (context) => {
7
+ const sid = context.agent?.session?.id ?? context.agent?.id;
8
+ return sid ? deps.systemSectionText(String(sid), context, "persona") : "";
9
+ },
10
+ }), "lume.persona-section()");
11
+ if (deps.layeredOn) {
12
+ for (const entry of deps.contexts) {
13
+ ctx.effect(() => ctx.systemPrompt.context({
14
+ name: entry.name,
15
+ order: entry.order,
16
+ text: (context) => {
17
+ const sid = context.agent?.session?.id ?? context.agent?.id;
18
+ return sid ? deps.runtimeContextText(String(sid), context, entry.part) : "";
19
+ },
20
+ }), `lume.runtime-context(${entry.name})`);
21
+ }
22
+ }
23
+ ctx.effect(() => ctx.systemPrompt.section({
24
+ name: deps.thinkingSection,
25
+ order: deps.thinkingOrder,
26
+ text: (context) => {
27
+ const sid = context.agent?.session?.id ?? context.agent?.id;
28
+ return sid ? deps.systemSectionText(String(sid), context, "thinking") : "";
29
+ },
30
+ }), "lume.thinking-section()");
31
+ ctx.effect(() => {
32
+ if (typeof ctx.systemPrompt?.context !== "function") {
33
+ ctx.logger?.warn?.("lume: 当前宿主不支持 systemPrompt.context,工具失败提示已跳过(不影响其余功能)");
34
+ return;
35
+ }
36
+ return ctx.systemPrompt.context({
37
+ name: deps.toolNoticeContext.name,
38
+ order: deps.toolNoticeContext.order,
39
+ text: (context) => {
40
+ const sid = context.agent?.session?.id ?? context.agent?.id;
41
+ const st = sid ? deps.runtime.get(String(sid)) : null;
42
+ if (!st)
43
+ return "";
44
+ return deps.buildToolFailureNotice({ failures: st.toolFailures, unknown: st.toolUnknown }) ?? "";
45
+ },
46
+ });
47
+ }, "lume.tool-notice-context()");
48
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 会话事件处理链路的**依赖契约**(架构整理 ①⑤:边界类型化 + 契约独立成模块)。
3
+ *
4
+ * 为什么单独成文件:事件分发(session-events)、轮边界(turn-boundary)、disposed 收尾共用同一份
5
+ * 依赖;契约放在使用方之一里会让另一个反向依赖它(成环)。这里只有类型,没有运行时依赖。
6
+ *
7
+ * 分组按**域**(env / notice / carrier / signal / prompt / tool / agent),组合用 extends;
8
+ * 访问保持扁平(deps.contractOf),所以分组不增加调用点噪音。
9
+ */
10
+ import * as citationsMod from "../core/citations.js";
11
+ import * as leakMod from "../core/leak-detector.js";
12
+ import * as ledgerMod from "../core/ledger.js";
13
+ import * as signalsMod from "../core/signals.js";
14
+ import * as textMod from "../core/text.js";
15
+ import * as knowledgeMod from "../core/knowledge.js";
16
+ import * as compactionMod from "./compaction.js";
17
+ import * as diagMod from "./diag.js";
18
+ import * as extractionMod from "./extraction.js";
19
+ import * as hostEventsMod from "./host-events.js";
20
+ import * as methodsMod from "./methods.js";
21
+ import * as noticesMod from "./notices.js";
22
+ import * as taskMemoryMod from "../core/task-memory.js";
23
+ import * as protocolMod from "./protocol.js";
24
+ import * as reflectionMod from "./reflection.js";
25
+ import * as thinkingMod from "./thinking.js";
26
+ import * as triggersMod from "./triggers.js";
27
+ import { handleTurnEnd } from "./turn-boundary.js";