dsh-plugin-prompt-tool 2.1.0 → 3.0.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.
Files changed (206) hide show
  1. package/README.md +100 -233
  2. package/engine/actions/append-context.mjs +3 -24
  3. package/engine/actions/assembly.mjs +70 -94
  4. package/engine/actions/catalog.mjs +65 -28
  5. package/engine/actions/content.mjs +8 -152
  6. package/engine/actions/examples.mjs +8 -4
  7. package/engine/actions/inbox-prepend.mjs +4 -5
  8. package/engine/actions/index.mjs +17 -9
  9. package/engine/actions/inject-text.mjs +24 -4
  10. package/engine/actions/pre-step-filter.mjs +3 -4
  11. package/engine/actions/registration.mjs +20 -28
  12. package/engine/actions/request-params.mjs +21 -1
  13. package/engine/actions/shared.mjs +22 -13
  14. package/engine/actions/stream-output.mjs +26 -0
  15. package/engine/actions/tool-gate.mjs +31 -0
  16. package/engine/actions/tool-result.mjs +36 -0
  17. package/engine/actions/tool-visibility.mjs +248 -0
  18. package/engine/agent-capability.mjs +67 -0
  19. package/engine/anchor-match.mjs +14 -21
  20. package/engine/audience.d.mts +2 -0
  21. package/engine/audience.mjs +102 -0
  22. package/engine/branch.d.mts +4 -0
  23. package/engine/branch.mjs +61 -18
  24. package/engine/compaction-epoch.d.mts +25 -0
  25. package/engine/compaction-epoch.mjs +27 -4
  26. package/engine/compositions/source/local/filesystem-editor.yml +3 -3
  27. package/engine/compositions/source/local/instruction-hint.yml +2 -4
  28. package/engine/compositions/source/local/module-file-tools.yml +2 -0
  29. package/engine/compositions/source/local/module-import-tools.yml +4 -0
  30. package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
  31. package/engine/compositions/source/local/module-rule-tools.yml +5 -0
  32. package/engine/compositions/source/local/module-variable-tools.yml +5 -0
  33. package/engine/compositions/source/local/present-as.yml +26 -0
  34. package/engine/compositions/source/local/tool-config-engine.yml +2 -3
  35. package/engine/compositions/source/local/tool-search.yml +11 -0
  36. package/engine/condition-facts.mjs +84 -0
  37. package/engine/conditions/anchor.mjs +63 -23
  38. package/engine/conditions/availability.mjs +1 -1
  39. package/engine/conditions/composite.mjs +3 -3
  40. package/engine/conditions/count.mjs +42 -11
  41. package/engine/conditions/evaluation.mjs +74 -19
  42. package/engine/conditions/examples.mjs +2 -1
  43. package/engine/conditions/index.d.mts +15 -0
  44. package/engine/conditions/index.mjs +23 -9
  45. package/engine/conditions/operators.mjs +4 -0
  46. package/engine/conditions/phase.mjs +9 -3
  47. package/engine/conditions/preset.mjs +1 -1
  48. package/engine/conditions/session.mjs +8 -2
  49. package/engine/conditions/subject.mjs +144 -65
  50. package/engine/conditions/text.mjs +26 -8
  51. package/engine/conditions/variable.mjs +83 -0
  52. package/engine/context-activation.d.mts +11 -0
  53. package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
  54. package/engine/delivery.d.mts +2 -0
  55. package/engine/delivery.mjs +93 -0
  56. package/engine/discovery.d.mts +38 -0
  57. package/engine/discovery.mjs +146 -0
  58. package/engine/env-safety.mjs +14 -0
  59. package/engine/executor.d.mts +28 -0
  60. package/engine/executor.mjs +290 -205
  61. package/engine/fillers.mjs +39 -8
  62. package/engine/fixed-registration.mjs +12 -0
  63. package/engine/history.mjs +71 -0
  64. package/engine/instruction-blocks.d.mts +13 -0
  65. package/engine/instruction-blocks.mjs +21 -0
  66. package/engine/instruction-files.d.mts +5 -0
  67. package/engine/instruction-files.mjs +97 -0
  68. package/engine/instruction-hint.mjs +69 -103
  69. package/engine/interpolate.d.mts +7 -0
  70. package/engine/interpolate.mjs +44 -13
  71. package/engine/layers.mjs +163 -227
  72. package/engine/machine-paths.mjs +40 -0
  73. package/engine/module-file-tools.mjs +8 -0
  74. package/engine/module-import-tools.mjs +10 -0
  75. package/engine/module-manifest-tools.mjs +8 -0
  76. package/engine/module-rule-tools.mjs +10 -0
  77. package/engine/module-variable-tools.mjs +10 -0
  78. package/engine/order.d.mts +14 -0
  79. package/engine/order.mjs +0 -3
  80. package/engine/path-safety.d.mts +2 -0
  81. package/engine/path-safety.mjs +58 -0
  82. package/engine/pre-step-source.d.mts +69 -0
  83. package/engine/pre-step-source.mjs +103 -0
  84. package/engine/present-as.mjs +77 -0
  85. package/engine/promotion-pool.d.mts +9 -0
  86. package/engine/promotion-pool.mjs +69 -0
  87. package/engine/record.mjs +2 -0
  88. package/engine/rule-engine.mjs +3 -2
  89. package/engine/rule-facts.d.mts +11 -0
  90. package/engine/rule-facts.mjs +60 -0
  91. package/engine/rule-runtime.d.mts +46 -0
  92. package/engine/rule-runtime.mjs +98 -13
  93. package/engine/rule-spec.d.mts +106 -0
  94. package/engine/rule-spec.mjs +301 -27
  95. package/engine/run-code-env.mjs +73 -19
  96. package/engine/schema.mjs +165 -243
  97. package/engine/sdk-strip.mjs +2 -2
  98. package/engine/service-names.d.mts +10 -0
  99. package/engine/service-names.mjs +10 -0
  100. package/engine/shared.d.mts +21 -0
  101. package/engine/shared.mjs +46 -13
  102. package/engine/skill-search.mjs +125 -53
  103. package/engine/source-kind.d.mts +9 -0
  104. package/engine/source-kind.mjs +16 -0
  105. package/engine/strategy-ids.mjs +5 -0
  106. package/engine/template-evaluation.d.mts +16 -0
  107. package/engine/template-evaluation.mjs +178 -0
  108. package/engine/template-frame.d.mts +16 -0
  109. package/engine/template-frame.mjs +69 -0
  110. package/engine/tool-config-engine.mjs +38 -34
  111. package/engine/tool-definition.d.mts +4 -0
  112. package/engine/tool-definition.mjs +5 -1
  113. package/engine/tool-git-bash.mjs +5 -1
  114. package/engine/tool-scope.mjs +22 -0
  115. package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
  116. package/engine/trigger.mjs +15 -270
  117. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  118. package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
  119. package/engine/vendor/yaml/package.json +1 -1
  120. package/lib/client.js +8312 -6724
  121. package/lib/client.js.map +1 -1
  122. package/lib/index.d.mts +16 -2120
  123. package/lib/index.mjs +26914 -5816
  124. package/modules/ponytail/module.yml +83 -203
  125. package/modules/ptc-mode/module.yml +45 -0
  126. package/modules/soul/assets/core-hard.md +6 -0
  127. package/modules/soul/assets/criteria.md +12 -0
  128. package/modules/soul/assets/echo.md +12 -0
  129. package/modules/soul/assets/layers.md +11 -0
  130. package/modules/soul/assets/ledger.md +9 -0
  131. package/modules/soul/assets/manifest.yml +20 -0
  132. package/modules/soul/assets/reference/00-layers.md +27 -0
  133. package/modules/soul/assets/reference/01-criteria.md +32 -0
  134. package/modules/soul/assets/reference/02-echo.md +161 -0
  135. package/modules/soul/assets/reference/03-vital.md +183 -0
  136. package/modules/soul/assets/reference/04-manifest.json +106 -0
  137. package/modules/soul/assets/reference/05-doctor.md +67 -0
  138. package/modules/soul/assets/reference/06-reference.md +189 -0
  139. package/modules/soul/assets/reference/07-bundle.json +431 -0
  140. package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
  141. package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
  142. package/modules/soul/assets/reference/10-echo-anatomy.md +109 -0
  143. package/modules/soul/assets/reference/11-self.md +80 -0
  144. package/modules/soul/assets/reference/12-ledger.md +159 -0
  145. package/modules/soul/assets/reference/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -0
  146. package/modules/soul/assets/reference/ECHO/345/205/255/345/261/202/344/270/216mnemon/345/215/217/345/220/214/350/257/264/346/230/216/344/271/246.md +229 -0
  147. package/modules/soul/assets/reference/README.md +12 -0
  148. package/modules/soul/assets/templates/agents-template.md +6 -0
  149. package/modules/soul/assets/vital.md +9 -0
  150. package/modules/soul/module.yml +311 -0
  151. package/modules/superpowers/module.yml +122 -0
  152. package/modules/tool-surface/module.yml +51 -47
  153. package/package.json +37 -40
  154. package/skills/dsh-module/SKILL.md +67 -147
  155. package/skills/dsh-module/reference.md +113 -86
  156. package/templates/10-pre-step.yml +2 -3
  157. package/templates/14-first-turn-anchor.yml +10 -14
  158. package/templates/15-guide-auto.yml +32 -28
  159. package/templates/16-custom-fallback.yml +33 -21
  160. package/templates/20-system-section.yml +1 -4
  161. package/templates/50-llm-stream.yml +4 -10
  162. package/templates/60-tool-pipeline.yml +12 -10
  163. package/templates/63-append-context.yml +4 -6
  164. package/templates/65-turn-stop.yml +2 -2
  165. package/templates/67-inbox-prepend.yml +2 -2
  166. package/templates/70-subagent-maintenance.yml +9 -10
  167. package/templates/75-subagent-end.yml +2 -2
  168. package/templates/80-tool-surface.yml +41 -57
  169. package/templates/tools/40-delegate-module-import.yml +22 -0
  170. package/templates/tools/50-delegate-module-rules.yml +14 -0
  171. package/templates/tools/60-delegate-module-variables.yml +17 -0
  172. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  173. package/engine/actions/decision.mjs +0 -54
  174. package/engine/actions/guard.mjs +0 -80
  175. package/engine/actions/sdk-strip.mjs +0 -30
  176. package/engine/character-tools.mjs +0 -8
  177. package/engine/classify-task.mjs +0 -53
  178. package/engine/compositions/source/local/character-tools.yml +0 -5
  179. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  180. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  181. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  182. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  183. package/engine/condition.mjs +0 -96
  184. package/engine/declared-triggers.mjs +0 -93
  185. package/engine/session-var-tools.mjs +0 -8
  186. package/engine/session-vars.mjs +0 -54
  187. package/engine/st-macros.d.mts +0 -22
  188. package/engine/st-macros.mjs +0 -208
  189. package/engine/st-render.mjs +0 -109
  190. package/engine/st-world-book.d.mts +0 -10
  191. package/engine/subagent-tool-policy-core.mjs +0 -400
  192. package/engine/subagent-tool-policy.mjs +0 -352
  193. package/engine/trigger-spec.mjs +0 -173
  194. package/engine/world-book-tools.mjs +0 -8
  195. package/lib/migrate-rules.d.mts +0 -1
  196. package/lib/migrate-rules.mjs +0 -610
  197. package/lib/module-package-DqJZaE71.mjs +0 -2
  198. package/lib/module-package-q2gDRu9H.mjs +0 -2869
  199. package/lib/rule-builder-DMrJten6.mjs +0 -14767
  200. package/templates/61-deny-tools-presentation.yml +0 -27
  201. package/templates/62-guard-deny-tools.yml +0 -20
  202. package/templates/policies/legacy-defaults.yml +0 -34
  203. package/templates/tools/40-delegate-character.yml +0 -15
  204. package/templates/tools/50-delegate-worldbook.yml +0 -15
  205. package/templates/tools/60-delegate-session-var.yml +0 -15
  206. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -1,20 +1,29 @@
1
- /** ST 世界书选择器;只消费显式 ST 导入字段,不改变原生 world-book 约定。 */
1
+ /** 原生上下文激活选择器;扫描、候选池与实际投递提交共用一条运行链。 */
2
2
  import { createAnchorMatcher } from './anchor-match.mjs'
3
- import { sessionEvents } from './shared.mjs'
4
- import { interpolateVariables } from './interpolate.mjs'
3
+ import { currentEvents, historyEvents } from './history.mjs'
4
+ import { evaluateTemplate } from './template-evaluation.mjs'
5
+ import { isConversationMessage } from './condition-facts.mjs'
6
+ import { isSuccessfulCompactionEnd } from './compaction-epoch.mjs'
5
7
 
6
- /** 只扫描真实对话的可见文本,排除插件注入与思维块。 */
7
- export function stChatMessages(session, pending = []) {
8
+ /** 只扫描真实对话的可见文本,排除插件注入与思维块(判据与 userText / 关键词 haystack 同源)。 */
9
+ export function conversationMessages(session, pending = []) {
8
10
  const result = [], ids = new Set()
9
11
  const add = (message, role) => {
10
- if (!message || !['user', 'assistant'].includes(role) || message.source?.plugin
11
- || (message.source?.kind && message.source.kind !== 'user')) return
12
+ if (!message || !['user', 'assistant'].includes(role) || !isConversationMessage(message)) return
12
13
  if (message.id && ids.has(message.id)) return
13
14
  if (message.id) ids.add(message.id)
14
15
  const text = (Array.isArray(message.content) ? message.content : []).filter(block => block.type === 'text' || block.type === undefined).map(block => block.text ?? '').join('')
15
16
  result.push({ id: message.id, role, text })
16
17
  }
17
- for (const event of sessionEvents(session)) {
18
+ // 关键词世界书由**模型看得见的**对话触发;被压缩遮蔽的旧对话不该再持续命中条目。
19
+ // 位置替换后同一 seq 可在 `surface.nodes` 里占多个位置(`history.mjs` 头部):无 id 的消息
20
+ // 按 id 去不掉,会重复计数并抬高 `chat.length`(它驱动 delay / sticky 窗口与概率 generation)。
21
+ const seenSeq = new Set()
22
+ for (const event of currentEvents(session)) {
23
+ if (event?.seq !== undefined) {
24
+ if (seenSeq.has(event.seq)) continue
25
+ seenSeq.add(event.seq)
26
+ }
18
27
  if (event?.type === 'user/message' || event?.type === 'assistant/message') add(event.data?.message ?? event.data, event.type.split('/')[0])
19
28
  }
20
29
  for (const message of pending) add(message, message.role ?? 'user')
@@ -22,6 +31,8 @@ export function stChatMessages(session, pending = []) {
22
31
  }
23
32
 
24
33
  const sessions = new WeakMap()
34
+ /** 每个会话最近一次成功压缩在日志里的位置;前推即窗口状态的重基准点。 */
35
+ const boundaries = new WeakMap()
25
36
  const matchers = new WeakMap()
26
37
  /** 每个会话只保留最近一次选择的诊断快照(随会话对象 GC 释放)。 */
27
38
  const lastRuns = new WeakMap()
@@ -32,12 +43,27 @@ const DIAGNOSTIC_LIMIT = 200
32
43
 
33
44
  /** 最近一次世界书选择诊断(只读快照;无记录时返回空集合,不触发任何求值)。
34
45
  * `evaluated: false` 表示该会话还没求值过,与"已求值但本次为空"区分开。 */
35
- export function lastWorldBookDiagnostics(session) {
46
+ export function lastContextActivationDiagnostics(session) {
36
47
  return lastRuns.get(session) ?? { records: [], truncated: false, step: 0, evaluated: false }
37
48
  }
38
49
 
39
- export function selectStWorldBook(configs, session, messages, warn = () => {}) {
40
- const entries = configs.filter(config => config.enabled !== false && config.strategy === 'world-book' && config.params?.stWorldBook)
50
+ /**
51
+ * 最近一次成功压缩在日志里的位置(-1 = 从未压缩)。
52
+ *
53
+ * 窗口状态(`last` 与 delay / sticky / cooldown 的基准)记的是当时的**绝对可见计数**,
54
+ * 而 `chat` 现在来自 `currentEvents`(可缩小的视图):成功压缩把可见面塌成一条摘要后计数骤降,
55
+ * 旧基准不重则 sticky 恒真(命中早已不可见的条目)、cooldown 被静默跳过。判据与 count / anchor
56
+ * 的失效点同源(`isSuccessfulCompactionEnd`);本模块的选择由批首直接调用、没有 observe 接线,
57
+ * 所以在批首比边界。倒序查最近一次,压缩之后的日志长度即扫描代价。
58
+ */
59
+ function lastCompactionBoundary(session) {
60
+ const events = historyEvents(session)
61
+ for (let index = events.length - 1; index >= 0; index--) if (isSuccessfulCompactionEnd(events[index])) return index
62
+ return -1
63
+ }
64
+
65
+ export function selectContextActivation(configs, session, messages, warn = () => {}, readVariables = () => ({ module: {}, global: {} })) {
66
+ const entries = configs.filter(config => config.enabled !== false && config.strategy === 'context-activation')
41
67
  // 本次选择的有界观测快照:`selection.diagnostics` 与会话最近快照引用**同一对象**,
42
68
  // 因此 commit 阶段追加记录或把 truncated 置真都写回同一份事实(不复制布尔值,
43
69
  // 否则 commit 越限时读取端仍看到 truncated=false)。
@@ -54,13 +80,20 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
54
80
  }
55
81
  // 被显式禁用的 ST 条目:由本层负责报告,不由 UI 猜测。
56
82
  for (const config of configs) {
57
- if (config.enabled === false && config.strategy === 'world-book' && config.params?.stWorldBook) note(config, 'excluded', 'disabled')
83
+ if (config.enabled === false && config.strategy === 'context-activation') note(config, 'excluded', 'disabled')
58
84
  }
59
85
  if (!entries.length) return finish(new Set())
60
- const chat = stChatMessages(session, messages)
86
+ const chat = conversationMessages(session, messages)
87
+ const batch = conversationMessages(undefined, messages)
61
88
  snapshot.step = chat.length
89
+ const boundary = lastCompactionBoundary(session)
62
90
  let state = sessions.get(session)
63
- if (!state) { state = new WeakMap(); sessions.set(session, state) }
91
+ // 边界前推说明可见视图被重写过(成功压缩)→ 丢弃本会话全部窗口,让每条条目按当前可见面重判。
92
+ if (boundaries.get(session) !== boundary) {
93
+ state = new WeakMap()
94
+ sessions.set(session, state)
95
+ boundaries.set(session, boundary)
96
+ }
64
97
  const generation = JSON.stringify([chat.length, chat.at(-1)])
65
98
  const selected = new Set(), failed = new Set(), occupied = new Set(), stickyEntries = new Set(), candidates = []
66
99
  const updates = new Map()
@@ -75,18 +108,21 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
75
108
  // 初始化即取走最小层级,因此「延迟到第 N 层」的条目在首次递归 pass 就能解锁;
76
109
  // 层级只增不减(:5131),已打开的层级对后续 pass 保持打开。
77
110
  const delayLevels = [...new Set(entries
78
- .map(config => config.params.stWorldBook.delayUntilRecursion)
111
+ .map(config => config.params.activation.delayUntilRecursion)
79
112
  .filter(Boolean)
80
113
  .map(value => value === true ? 1 : value))].sort((a, b) => a - b)
81
114
  let delayLevel = delayLevels.shift() ?? 0
82
- const hasRecursive = entries.some(config => config.params.stWorldBook.recursive === true)
115
+ // 池是全批共享的(对齐 ST `world_info_recursive` 的全局扫描缓冲:新入选正文都进同一个池),
116
+ // 这里只决定「池是否产生内容、pass 是否由递归驱动」(层级池推进依赖后者,缺省时既有语义不变);
117
+ // 谁参与重扫按模块判,来源格式中的书级或条目级递归记录不参与运行。
118
+ const recursive = entries.some(config => config.contextActivationRecursive === true)
83
119
  /** 键匹配器(按 params 对象缓存;签名含插值后的键与匹配选项)。 */
84
120
  const matcherOf = (config, material) => {
85
121
  const p = config.params
86
- const signature = JSON.stringify([material.keys, material.secondaryKeys, p.caseSensitive, p.wholeWords, p.useRegex])
122
+ const signature = JSON.stringify([material.keys, material.secondaryKeys, p.caseSensitive, p.wholeWords, p.useRegex, p.wordBoundary])
87
123
  let compiled = matchers.get(p)
88
124
  if (compiled?.signature !== signature) {
89
- compiled = { signature, matcher: createAnchorMatcher({ keys: material.keys, secondaryKeys: material.secondaryKeys, caseSensitive: p.caseSensitive === true, wholeWords: p.wholeWords === true, useRegex: p.useRegex, stWords: true }) }
125
+ compiled = { signature, matcher: createAnchorMatcher({ keys: material.keys, secondaryKeys: material.secondaryKeys, caseSensitive: p.caseSensitive === true, wholeWords: p.wholeWords === true, useRegex: p.useRegex, wordBoundary: p.wordBoundary }) }
90
126
  matchers.set(p, compiled)
91
127
  }
92
128
  return compiled.matcher
@@ -98,122 +134,126 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
98
134
  // 每 pass 的扫描材料缓存:递归文本随 pass 变化,材料必须按 pass 重新构造;
99
135
  // 材料只被匹配与评分读取,构造本身不改变入选结果。
100
136
  const scanCache = new Map()
101
- const scanOf = (config, st) => {
137
+ const scanOf = (config, activation) => {
102
138
  let material = scanCache.get(config)
103
139
  if (material !== undefined) return material
104
- const depth = count(st.scanDepth, 2)
105
- const parts = depth === 0 ? [] : chat.slice(-depth).reverse().map(message => message.text)
106
- // 角色字段扫描(ST globalScanData,world-info.js:294-320):每个开关只在对应变量
107
- // 有值时并入扫描文本;creator_notes / depth_prompt 由导入期登记(未开启不并入,
108
- // 既有触发面不变)。
109
- for (const [flag, key] of [['matchCharacterDescription', 'description'], ['matchCharacterPersonality', 'personality'], ['matchScenario', 'scenario'], ['matchPersonaDescription', 'persona'], ['matchCreatorNotes', 'creator_notes'], ['matchCharacterDepthPrompt', 'depth_prompt']]) {
110
- if (depth > 0 && st[flag] === true && typeof config.variables[key] === 'string') parts.push(config.variables[key])
140
+ const history = activation.scan === 'history'
141
+ const depth = count(activation.scanDepth, history ? 2 : batch.length)
142
+ const parts = depth === 0 ? [] : (history ? chat : batch).slice(-depth).reverse().map(message => message.text)
143
+ const variables = readVariables(config)
144
+ const render = (text, source) => evaluateTemplate(text, { ...variables, session, sourceId: `${config.sourceModuleId ?? ''}:${config.id}:${source}`, readOnly: true }).text
145
+ for (const key of activation.scanVariables ?? []) {
146
+ const value = Object.hasOwn(variables.module, key) ? variables.module[key] : Object.hasOwn(variables.global, key) ? variables.global[key] : undefined
147
+ if (depth > 0 && typeof value === 'string') parts.push(render(value, `scan:${key}`))
111
148
  }
149
+ // 池是全批共享的;开关没开的模块靠下面的 pass 门控不参与重扫(隔离不靠分池)。
112
150
  if (pass > 0 && depth > 0) parts.push(...recursiveText)
113
151
  const p = config.params
114
- const keys = (Array.isArray(p.keys) ? p.keys : []).map(key => interpolateVariables(String(key), config.variables, session)).filter(Boolean)
152
+ const keys = (p.keys ?? []).map((key, index) => render(key, `keys:${index}`)).filter(Boolean)
115
153
  // 空宏仍是一项未命中的副键:匹配器忽略空值,但 AND_ALL/NOT_ALL 的总数不能缩水。
116
- const secondaryKeys = (Array.isArray(p.secondaryKeys) ? p.secondaryKeys : []).map(key => interpolateVariables(String(key), config.variables, session))
154
+ const secondaryKeys = (p.secondaryKeys ?? []).map((key, index) => render(key, `secondaryKeys:${index}`))
117
155
  material = { depth, keys, secondaryKeys, text: parts.length ? '\x01' + parts.join('\n\x01') : '' }
118
156
  scanCache.set(config, material)
119
157
  return material
120
158
  }
121
159
  // ST getScore 等价(world-info.js:428-473):只统计命中数,否定逻辑(NOT_ALL/NOT_ANY)
122
160
  // 不参与加分,主键为空记 0 分。
123
- const scoreOf = (config, st) => {
124
- const material = scanOf(config, st)
161
+ const scoreOf = (config, activation) => {
162
+ const material = scanOf(config, activation)
125
163
  const match = matcherOf(config, material).scan(material.text)
126
164
  if (material.keys.length === 0) return 0
127
165
  if (material.secondaryKeys.length === 0) return match.primary
128
- const logic = config.params.selectiveLogic ?? 0
129
- if (logic === 0) return match.primary + match.secondary
130
- if (logic === 3) return match.secondary === material.secondaryKeys.length ? match.primary + match.secondary : match.primary
166
+ const logic = config.params.selectiveLogic ?? 'AND_ANY'
167
+ if (logic === 'AND_ANY') return match.primary + match.secondary
168
+ if (logic === 'AND_ALL') return match.secondary === material.secondaryKeys.length ? match.primary + match.secondary : match.primary
131
169
  return match.primary
132
170
  }
133
171
  for (const config of priority) {
134
172
  if (selected.has(config) || failed.has(config)) continue
135
- const p = config.params, st = p.stWorldBook
173
+ const p = config.params, activation = p.activation
136
174
  const previous = state.get(config)
137
- const sticky = previous?.last !== undefined && count(st.sticky) > 0 && chat.length < previous.last + count(st.sticky)
175
+ const sticky = previous?.last !== undefined && count(activation.sticky) > 0 && chat.length < previous.last + count(activation.sticky)
138
176
  if (sticky) { stickyEntries.add(config); note(config, 'candidate', 'sticky') }
139
- if (chat.length < count(st.delay)) { note(config, 'excluded', 'delay', { delay: count(st.delay), messages: chat.length }); continue }
140
- if (!sticky && previous?.last !== undefined && count(st.cooldown) > 0 && chat.length > previous.last
141
- && chat.length < previous.last + count(st.sticky) + count(st.cooldown)) { note(config, 'excluded', 'cooldown'); continue }
177
+ if (chat.length < count(activation.delay)) { note(config, 'excluded', 'delay', { delay: count(activation.delay), messages: chat.length }); continue }
178
+ if (!sticky && previous?.last !== undefined && count(activation.cooldown) > 0 && chat.length > previous.last
179
+ && chat.length < previous.last + count(activation.sticky) + count(activation.cooldown)) { note(config, 'excluded', 'cooldown'); continue }
142
180
  // ST 的延迟门控(world-info.js:4860-4868):非递归 pass 一律抑制(sticky 例外,
143
181
  // constant 也不例外);递归 pass 中层级未到同样抑制。比较用条目原始值。
144
- const delayUntil = st.delayUntilRecursion
182
+ const delayUntil = activation.delayUntilRecursion
145
183
  if (!sticky && delayUntil && (pass === 0 || delayUntil > delayLevel)) {
146
184
  note(config, 'excluded', 'delay-until-recursion', { delayUntilRecursion: delayUntil, level: delayLevel, pass })
147
185
  continue
148
186
  }
149
- // 递归 pass 的参与资格:excludeRecursion 对任何条目生效;其余条目仍要求自身声明
150
- // recursive(延迟条目由层级池驱动,ST 的门控不要求全局 recursive)。
151
- if (pass > 0 && (st.excludeRecursion === true || (!delayUntil && st.recursive !== true))) { note(config, 'excluded', 'recursion'); continue }
187
+ // 递归 pass 的参与资格:条目自己的 excludeRecursion(ST world-info.js:4870)与本模块开关
188
+ // 都要过。延迟条目由层级池驱动——层级池是全批时钟(层级只增不减),与递归正文无关,
189
+ // 因此不受模块递归开关门控,开关缺省时它的既有语义不变。
190
+ if (pass > 0 && activation.excludeRecursion === true) { note(config, 'excluded', 'recursion'); continue }
191
+ if (pass > 0 && config.contextActivationRecursive !== true && !delayUntil) { note(config, 'excluded', 'recursion'); continue }
152
192
  let active = sticky || p.constant === true
153
- let activation = sticky ? 'sticky' : p.constant === true ? 'constant' : 'key-match'
193
+ const reason = sticky ? 'sticky' : p.constant === true ? 'constant' : 'key-match'
154
194
  if (!active) {
155
- const material = scanOf(config, st)
156
195
  try {
196
+ const material = scanOf(config, activation)
157
197
  const match = matcherOf(config, material).scan(material.text)
158
198
  active = match.primary > 0
159
- const logic = p.selectiveLogic ?? 0
199
+ const logic = p.selectiveLogic ?? 'AND_ANY'
160
200
  const detail = { scanDepth: material.depth, primary: match.primary, secondary: match.secondary, secondaryKeys: material.secondaryKeys.length, logic }
161
- if (active && st.selective === true && material.secondaryKeys.length > 0) {
162
- active = logic === 1 ? match.secondary < material.secondaryKeys.length : logic === 2 ? match.secondary === 0
163
- : logic === 3 ? match.secondary === material.secondaryKeys.length : match.secondary > 0
201
+ if (active && activation.selective === true && material.secondaryKeys.length > 0) {
202
+ active = logic === 'NOT_ALL' ? match.secondary < material.secondaryKeys.length : logic === 'NOT_ANY' ? match.secondary === 0
203
+ : logic === 'AND_ALL' ? match.secondary === material.secondaryKeys.length : match.secondary > 0
164
204
  if (!active) note(config, 'rejected', 'secondary-miss', detail)
165
205
  } else if (!active) {
166
206
  note(config, 'rejected', 'primary-miss', detail)
167
207
  }
168
208
  } catch (error) {
169
209
  note(config, 'rejected', 'match-error', { message: String(error?.message ?? error).slice(0, 120) })
170
- warn(`prompt-config-engine: 世界书 ${config.id} 匹配失败:${String(error?.message ?? error)}`)
210
+ warn(`prompt-config-engine: 上下文激活 ${config.id} 匹配失败:${String(error?.message ?? error)}`)
171
211
  failed.add(config)
172
212
  continue
173
213
  }
174
214
  }
175
215
  if (!active) continue
176
- const probability = typeof st.probability === 'number' && Number.isFinite(st.probability) ? Math.max(0, Math.min(100, st.probability)) : 100
216
+ const probability = typeof activation.probability === 'number' && Number.isFinite(activation.probability) ? Math.max(0, Math.min(100, activation.probability)) : 100
177
217
  const roll = previous?.generation === generation ? previous.roll : Math.random() * 100
178
218
  state.set(config, { ...previous, generation, roll })
179
- if (!sticky && st.useProbability !== false && (probability <= 0 || roll >= probability)) {
219
+ if (!sticky && activation.useProbability !== false && (probability <= 0 || roll >= probability)) {
180
220
  note(config, 'rejected', 'probability', { probability, roll })
181
221
  failed.add(config)
182
222
  continue
183
223
  }
184
- note(config, 'candidate', activation, { probability, useProbability: st.useProbability !== false })
224
+ note(config, 'candidate', reason, { probability, useProbability: activation.useProbability !== false })
185
225
  candidates.push(config)
186
226
  }
187
227
  if (!candidates.length && !delayLevels.length) break
188
228
  const groups = new Map()
189
229
  for (const config of candidates) {
190
- const names = String(config.params.stWorldBook.group ?? '').split(',').map(name => name.trim()).filter(Boolean)
230
+ const names = String(config.params.activation.selectionGroup ?? '').split(',').map(name => name.trim()).filter(Boolean)
191
231
  if (!names.length) { selected.add(config); note(config, 'selected', 'ungrouped'); continue }
192
232
  if (names.some(name => occupied.has(name))) { note(config, 'rejected', 'group-occupied'); failed.add(config); continue }
193
233
  for (const name of names) { const members = groups.get(name) ?? []; members.push(config); groups.set(name, members) }
194
234
  }
195
235
  for (const [name, members] of groups) {
196
236
  if (occupied.has(name)) continue
197
- let remaining = members.filter(config => !failed.has(config) && !String(config.params.stWorldBook.group).split(',').some(group => occupied.has(group.trim())))
237
+ let remaining = members.filter(config => !failed.has(config) && !String(config.params.activation.selectionGroup).split(',').some(group => occupied.has(group.trim())))
198
238
  if (!remaining.length) continue
199
239
  const sticky = remaining.filter(config => stickyEntries.has(config))
200
240
  // 组内评分(ST filterGroupsByScoring 5292-5328):无全局开关时,只要组内存在显式
201
241
  // 开启 useGroupScoring 的条目就整组参与评分;组内有 sticky 时整组跳过(5300-5305)。
202
242
  // 只有开启评分的条目会被淘汰(严格小于最高分);未开启者不被淘汰,但其分数计入最高分。
203
243
  const scoreOut = new Set()
204
- if (!sticky.length && remaining.some(config => config.params.stWorldBook.useGroupScoring === true)) {
244
+ if (!sticky.length && remaining.some(config => config.params.activation.useGroupScoring === true)) {
205
245
  let scores
206
246
  try {
207
- scores = remaining.map(config => scoreOf(config, config.params.stWorldBook))
247
+ scores = remaining.map(config => scoreOf(config, config.params.activation))
208
248
  } catch (error) {
209
249
  scores = undefined
210
- warn(`prompt-config-engine: 世界书分组评分失败:${String(error?.message ?? error)}`)
250
+ warn(`prompt-config-engine: 上下文激活分组评分失败:${String(error?.message ?? error)}`)
211
251
  }
212
252
  if (scores !== undefined) {
213
253
  const maxScore = Math.max(...scores)
214
254
  const survivors = []
215
255
  for (const [index, config] of remaining.entries()) {
216
- if (config.params.stWorldBook.useGroupScoring !== true || scores[index] >= maxScore) { survivors.push(config); continue }
256
+ if (config.params.activation.useGroupScoring !== true || scores[index] >= maxScore) { survivors.push(config); continue }
217
257
  note(config, 'rejected', 'group-score-lost', { group: name, score: scores[index], maxScore })
218
258
  scoreOut.add(config)
219
259
  failed.add(config)
@@ -222,17 +262,17 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
222
262
  if (!remaining.length) continue
223
263
  }
224
264
  }
225
- const overriding = sticky.length ? sticky : remaining.filter(config => config.params.stWorldBook.groupOverride === true)
265
+ const overriding = sticky.length ? sticky : remaining.filter(config => config.params.activation.groupOverride === true)
226
266
  const available = overriding.length ? overriding : remaining
227
- const weight = config => typeof config.params.stWorldBook.groupWeight === 'number' ? Math.max(0, config.params.stWorldBook.groupWeight) : 100
267
+ const weight = config => typeof config.params.activation.groupWeight === 'number' ? Math.max(0, config.params.activation.groupWeight) : 100
228
268
  let choice = available[0]
229
269
  if (!overriding.length) {
230
270
  let roll = (state.get(choice).roll / 100) * available.reduce((sum, config) => sum + weight(config), 0)
231
271
  for (const config of available) { roll -= weight(config); if (roll < 0) { choice = config; break } }
232
272
  }
233
273
  selected.add(choice)
234
- note(choice, 'selected', 'group-winner', { group: String(choice.params.stWorldBook.group ?? '') })
235
- for (const group of String(choice.params.stWorldBook.group).split(',').map(value => value.trim()).filter(Boolean)) occupied.add(group)
274
+ note(choice, 'selected', 'group-winner', { group: String(choice.params.activation.selectionGroup ?? '') })
275
+ for (const group of String(choice.params.activation.selectionGroup).split(',').map(value => value.trim()).filter(Boolean)) occupied.add(group)
236
276
  for (const config of members) {
237
277
  // 已按评分淘汰的成员只记 group-score-lost:它没有参与最后的组内竞争,
238
278
  // 再记一条 group-lost 会让诊断给出两个互相矛盾的原因。
@@ -246,11 +286,11 @@ export function selectStWorldBook(configs, session, messages, warn = () => {}) {
246
286
  for (const config of added) {
247
287
  const previous = state.get(config)
248
288
  // sticky 期间的重发不延长有效期;新命中才开始下一段窗口。
249
- if (previous.last === undefined || chat.length >= previous.last + count(config.params.stWorldBook.sticky)) updates.set(config, { ...previous, last: chat.length })
289
+ if (previous.last === undefined || chat.length >= previous.last + count(config.params.activation.sticky)) updates.set(config, { ...previous, last: chat.length })
250
290
  }
251
291
  // 递归驱动优先于延迟层级(ST world-info.js:5097-5133):有新正文可递归时层级保持不变;
252
292
  // 没有新正文而层级池仍有剩余时打开下一层继续扫描;两者都没有才结束。
253
- const recursiveAdds = hasRecursive ? added.filter(config => config.params.stWorldBook.preventRecursion !== true) : []
293
+ const recursiveAdds = recursive ? added.filter(config => config.params.activation.preventRecursion !== true) : []
254
294
  if (recursiveAdds.length) recursiveText.push(...recursiveAdds.map(config => config.texts.join('\n')))
255
295
  else if (delayLevels.length) delayLevel = delayLevels.shift()
256
296
  else break
@@ -0,0 +1,2 @@
1
+ /** 宿主使用的投递身份公开面。 */
2
+ export function identityOf(config: Record<string, unknown>): string
@@ -0,0 +1,93 @@
1
+ import { historyEvents, viewRestricted } from './history.mjs'
2
+ import { pluginKind, pluginOwner } from './source-kind.mjs'
3
+
4
+ export function eventMessage(event) {
5
+ const data = event?.data
6
+ if (data === null || typeof data !== 'object') return undefined
7
+ const message = data.message !== null && typeof data.message === 'object' ? data.message : data
8
+ return message?.source !== null && typeof message?.source === 'object' ? message : undefined
9
+ }
10
+
11
+ export function moduleIdOf(value, field) {
12
+ const id = value?.[field]
13
+ return typeof id === 'string' && id.length > 0 ? id : undefined
14
+ }
15
+
16
+ export const moduleOf = source => moduleIdOf(source, 'moduleId')
17
+ export const identityOf = config => config.mergeMode === 'merged' ? `merged:${config.position}` : config.identity.value
18
+
19
+ export function pluginIdentityOf(source) {
20
+ if (typeof source?.plugin === 'string' && source.plugin.length > 0) return source.plugin
21
+ return pluginOwner(source?.kind)
22
+ }
23
+
24
+ export function identityStampOf(config) {
25
+ const plugin = identityOf(config)
26
+ const moduleId = moduleIdOf(config, 'sourceModuleId')
27
+ return {
28
+ kind: typeof config.sourceKind === 'string' && config.sourceKind.length > 0 ? config.sourceKind : pluginKind(plugin),
29
+ plugin,
30
+ ...(moduleId === undefined ? {} : { moduleId }),
31
+ }
32
+ }
33
+
34
+ export function identityHit(source, config, value) {
35
+ if (source?.kind !== config.sourceKind && pluginIdentityOf(source) !== value) return false
36
+ const moduleId = moduleOf(source)
37
+ return moduleId === undefined || moduleId === moduleIdOf(config, 'sourceModuleId')
38
+ }
39
+
40
+ /** 投递关联与去重读取相同的可见内容,投影删除的消息不能再充当事实。 */
41
+ export function visibleMessages(session) {
42
+ const events = historyEvents(session)
43
+ const visible = viewRestricted(session, events) ? session.surface.nodes.map(seq => events[seq]) : events
44
+ const seen = new Set()
45
+ const out = []
46
+ for (const event of visible) {
47
+ const seq = Number.isSafeInteger(event.seq) ? event.seq : events.indexOf(event)
48
+ if (seq < 0 || seen.has(seq)) continue
49
+ seen.add(seq)
50
+ const message = typeof session?.surface?.deriveEventMessage === 'function'
51
+ ? session.surface.deriveEventMessage(event) : eventMessage(event)
52
+ if (message != null) out.push({ event, seq, message })
53
+ }
54
+ return out
55
+ }
56
+
57
+ export function injectedMessage(config, session) {
58
+ const value = identityOf(config)
59
+ return visibleMessages(session).find(({ message, seq }) => identityHit(message.source, config, value)
60
+ && (!config.linkedDelivery || message.source.sessionId === session.id && seq >= (session.inheritedEventCount ?? 0)))?.message
61
+ }
62
+
63
+ export function actionStamp(config, session) {
64
+ return config.ruleId === undefined || config.ruleActionId === undefined || session?.id === undefined ? {} : {
65
+ ruleId: config.ruleId, actionId: config.ruleActionId, sessionId: session?.id,
66
+ }
67
+ }
68
+
69
+ /** 官方同 order 按 name 排序,注册和投递查询共用这一份名字。 */
70
+ export function textRegistrationName(config, field) {
71
+ const explicit = config.params?.[field]
72
+ if (typeof explicit === 'string' && explicit.length > 0) return explicit
73
+ if (config.sourceModuleId === undefined) return config.id
74
+ return `prompt-tool:${String(config.sequence).padStart(16, '0')}:${config.sourceModuleId}:${config.id}`
75
+ }
76
+
77
+ /** after 需要动作的严格证据;去重的宽松身份命中不能代替这份关联。 */
78
+ export function findActionDelivery(session, config, messages = visibleMessages(session)) {
79
+ if (config === undefined || session?.id === undefined) return undefined
80
+ let found
81
+ for (const entry of messages) {
82
+ if (entry.seq < (session?.inheritedEventCount ?? 0)) continue
83
+ const source = entry.message.source
84
+ const matches = config.layer === 'runtime-context'
85
+ ? source?.kind === 'runtime-context' && source.form === 'snapshot'
86
+ && source.sections?.some(section => section.name === textRegistrationName(config, 'contextName') && typeof section.text === 'string' && section.text.length > 0)
87
+ : (source?.moduleId ?? '') === (config.sourceModuleId ?? '')
88
+ && source?.ruleId === config.ruleId && source?.actionId === config.ruleActionId
89
+ && source?.sessionId === session?.id
90
+ if (matches && (found === undefined || entry.seq > found.seq)) found = entry
91
+ }
92
+ return found
93
+ }
@@ -0,0 +1,38 @@
1
+ /** `discovery.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
2
+
3
+ export const DISCOVERY_ADAPTER_ROWS: Readonly<Record<string, string>>
4
+ export const UNLOCKABLE_DISCOVERY_KINDS: ReadonlySet<string>
5
+
6
+ export interface DiscoveryUnlock {
7
+ tool: string
8
+ key: string
9
+ }
10
+
11
+ export interface DiscoveryAdapterMetadata {
12
+ kind: string
13
+ tool: string
14
+ unlock?: DiscoveryUnlock
15
+ }
16
+
17
+ export interface DiscoveryRegistry {
18
+ register(agent: unknown, kind: string, adapter: DiscoveryAdapterMetadata): () => void
19
+ get(agent: unknown, kind: string): DiscoveryAdapterMetadata | undefined
20
+ }
21
+
22
+ export function createDiscoveryRegistry(): DiscoveryRegistry
23
+ export function assertDiscoveryRows(rules: readonly unknown[], moduleIds: readonly string[] | undefined, moduleId: string): void
24
+ export function queryTokens(query: unknown): string[]
25
+ export function scoreRows(items: readonly unknown[], tokens: readonly string[], options?: {
26
+ nameOf?: (item: unknown) => unknown
27
+ haystackOf?: (item: unknown) => unknown
28
+ }): Array<{ item: unknown; name: string; score: number; exact: boolean }>
29
+ export function rankRows<T extends { exact: boolean; score: number; name: string }>(rows: readonly T[], admit?: (row: T) => boolean): T[]
30
+ export function renderRows(rows: readonly unknown[], options?: {
31
+ query?: string
32
+ limit?: number
33
+ noun?: string
34
+ lineOf?: (row: unknown) => string
35
+ emptyText?: string
36
+ footer?: string
37
+ warnings?: readonly string[]
38
+ }): string[]
@@ -0,0 +1,146 @@
1
+ /**
2
+ * discovery —— 发现引擎:搜索、排序、截断渲染与「适配器注册表」的唯一实现。
3
+ *
4
+ * 两个数据源(工具目录 / 技能目录)共用:
5
+ * - 分词与候选打分(queryTokens / scoreRows);
6
+ * - 准入与排序(rankRows:默认 any-token,两个发现面同一口径;调用方可给更严的 admit);
7
+ * - 截断、空结果与页脚(renderRows)。
8
+ *
9
+ * 引擎之间通过 **cordis 服务** 互相调用,而不是直接 import 实现:
10
+ * - 宿主 root provide `promptToolDiscovery`(createDiscoveryRegistry);
11
+ * - adapter 行的 apply 在拿到 { agent } 时注册自己的元数据;
12
+ * - 消费方(tool-visibility)在首次装配时用 `ctx.get(DISCOVERY_SERVICE)` 按 agent 解析。
13
+ * 服务读取用 `ctx.get`(不需要 inject;cordis reflect.ts 的 get 明确「without the inject
14
+ * requirement」),属性读才需要 inject 且沿祖先链——这里刻意用 get。
15
+ *
16
+ * 注册表只存**元数据**(kind / tool / unlock / adapter 身份),工具仍由 adapter 行自己注册:
17
+ * 行的语义不变(挂行 = 有该工具),消费方只消费元数据(解锁名单从哪个参数回收、该用哪个工具名)。
18
+ */
19
+
20
+ /** 每个 agent 的 adapter 只保留一份:同一个 adapter 重复注册是幂等的。 */
21
+ export function createDiscoveryRegistry() {
22
+ const byAgent = new WeakMap()
23
+ const assertAdapter = (kind, adapter) => {
24
+ if (typeof kind !== 'string' || kind.length === 0) throw new TypeError('discovery adapter kind must be a non-empty string')
25
+ if (adapter === null || typeof adapter !== 'object') throw new TypeError(`discovery adapter "${kind}" must be an object`)
26
+ if (typeof adapter.tool !== 'string' || adapter.tool.length === 0) {
27
+ throw new TypeError(`discovery adapter "${kind}" needs a tool name`)
28
+ }
29
+ if (adapter.unlock !== undefined) {
30
+ for (const field of ['tool', 'key']) {
31
+ if (typeof adapter.unlock[field] !== 'string' || adapter.unlock[field].length === 0) {
32
+ throw new TypeError(`discovery adapter "${kind}" unlock.${field} must be a non-empty string`)
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return {
38
+ /**
39
+ * 注册一份 adapter 元数据;同一 agent 同一 kind 只允许一个实现。
40
+ * 同一 adapter 身份重复注册(同一行在重绑/多次装配中再次 apply)是幂等的。
41
+ */
42
+ register(agent, kind, adapter) {
43
+ if (agent === undefined || agent === null) return () => {}
44
+ assertAdapter(kind, adapter)
45
+ let entries = byAgent.get(agent)
46
+ if (entries === undefined) {
47
+ entries = new Map()
48
+ byAgent.set(agent, entries)
49
+ }
50
+ const existing = entries.get(kind)
51
+ if (existing !== undefined) {
52
+ if (existing === adapter) return () => {}
53
+ throw new Error(`discovery kind "${kind}" is already registered for this agent`)
54
+ }
55
+ entries.set(kind, adapter)
56
+ return () => { if (entries.get(kind) === adapter) entries.delete(kind) }
57
+ },
58
+ /** 解析某 agent 的 adapter 元数据;没有注册过时返回 undefined(发现是可选能力)。 */
59
+ get(agent, kind) {
60
+ if (agent === undefined || agent === null) return undefined
61
+ return byAgent.get(agent)?.get(kind)
62
+ },
63
+ }
64
+ }
65
+
66
+ /** kind → 提供该 adapter 的组合行 id(准备期静态校验:声明了 discovery 就必须列对应行)。 */
67
+ export const DISCOVERY_ADAPTER_ROWS = Object.freeze({
68
+ tools: 'tool-search',
69
+ skills: 'skill-search',
70
+ })
71
+
72
+ /** 可按名解锁的 kind:技能面只有搜索/加载,没有「解锁名单」语义。 */
73
+ export const UNLOCKABLE_DISCOVERY_KINDS = new Set(['tools'])
74
+
75
+ /**
76
+ * 准备期校验(纯函数,供 agent-assembly 调用):规则里出现的 discovery.kind 必须有对应行。
77
+ * 失败信息点明缺哪一行,避免「装配成功但发现能力静默缺失」。
78
+ */
79
+ export function assertDiscoveryRows(rules, moduleIds, moduleId) {
80
+ const ids = new Set(Array.isArray(moduleIds) ? moduleIds : [])
81
+ for (const rule of Array.isArray(rules) ? rules : []) {
82
+ // 未启用的规则不装配,也就不需要适配器行;启用后缺行会在下一次准备期失败。
83
+ if (rule?.enabled === false) continue
84
+ for (const action of Array.isArray(rule?.actions) ? rule.actions : []) {
85
+ const discovery = action?.discovery
86
+ if (discovery === undefined) continue
87
+ const kind = discovery.kind
88
+ const row = DISCOVERY_ADAPTER_ROWS[kind]
89
+ if (row === undefined) {
90
+ throw new TypeError(`模块 ${moduleId}:未知 discovery kind ${JSON.stringify(kind)} — 可用值:${Object.keys(DISCOVERY_ADAPTER_ROWS).join(', ')}`)
91
+ }
92
+ if (!ids.has(row)) {
93
+ throw new TypeError(`模块 ${moduleId} 声明 discovery kind "${kind}",但 modules 未包含 "${row}" 行 — 发现适配器必须作为引擎行装配`)
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ /** 查询切成小写 token(Unicode 字母数字与下划线连字符;两个数据源同一口径)。 */
100
+ export function queryTokens(query) {
101
+ return (typeof query === 'string' ? query : '').toLowerCase().split(/[^\p{L}\p{N}_-]+/u).filter(Boolean)
102
+ }
103
+
104
+ /**
105
+ * 候选打分:每个 token 在 haystack 里出现一次记 1 分;空查询返回空数组
106
+ * (调用方据此区分「没给查询」与「没搜到」)。
107
+ */
108
+ export function scoreRows(items, tokens, { nameOf, haystackOf } = {}) {
109
+ if (!Array.isArray(items) || tokens.length === 0) return []
110
+ const name = nameOf ?? ((item) => item?.name)
111
+ const haystack = haystackOf ?? ((item) => `${item?.name ?? ''} ${item?.description ?? ''}`)
112
+ return items
113
+ .map((item) => {
114
+ const itemName = String(name(item) ?? '')
115
+ const text = String(haystack(item) ?? '').toLowerCase()
116
+ let score = 0
117
+ for (const token of tokens) if (text.includes(token)) score += 1
118
+ return { item, name: itemName, score, exact: tokens.includes(itemName.toLowerCase()) }
119
+ })
120
+ .filter((row) => row.name.length > 0)
121
+ }
122
+
123
+ /**
124
+ * 准入与排序:admit 决定哪些行算命中(默认 any-token,工具面与技能面同口径);
125
+ * 精确名优先,其次命中数,同分按名字字典序。
126
+ */
127
+ export function rankRows(rows, admit = (row) => row.exact || row.score >= 1) {
128
+ return rows
129
+ .filter(admit)
130
+ .sort((a, b) => (b.exact - a.exact) || (b.score - a.score) || a.name.localeCompare(b.name))
131
+ }
132
+
133
+ /**
134
+ * 渲染命中:标题、逐行、截断提示、页脚;空结果只回 emptyText。
135
+ * lineOf 负责单个条目的格式(描述截断长度等由各 adapter 决定)。
136
+ */
137
+ export function renderRows(rows, { query, limit, noun, lineOf, emptyText, footer, warnings = [] } = {}) {
138
+ const matches = rows.slice(0, limit)
139
+ if (matches.length === 0) return [emptyText]
140
+ const lines = [`Matching ${noun} (${matches.length}${rows.length > limit ? ` of ${rows.length}` : ''}):`]
141
+ for (const row of matches) lines.push(lineOf(row))
142
+ if (rows.length > limit) lines.push(`(truncated at ${limit} — add tokens to narrow the query)`)
143
+ for (const warning of warnings) lines.push(warning)
144
+ if (footer !== undefined && footer.length > 0) lines.push(footer)
145
+ return lines
146
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * env 事实的安全边界:env-facts 只能读白名单里的环境变量,不能把任意 `process.env` 搬进提示词。
3
+ * 白名单与凭据形态判据是导出常量,供填充器与测试共用同一份事实。
4
+ */
5
+
6
+ /** 疑似凭据的名字一律不放行。 */
7
+ export const SENSITIVE_ENV_RE = /KEY|PASSWORD|SECRET|TOKEN/i
8
+
9
+ /** env-facts 可读取的键:run-code-env 默认白名单 + 路径类事实(CWD/WORKSPACE)。 */
10
+ export const SAFE_ENV_KEYS = new Set([
11
+ 'PATH', 'PATHEXT', 'HOME', 'USERPROFILE', 'USERNAME', 'COMPUTERNAME', 'OS', 'TEMP', 'TMP',
12
+ 'SystemRoot', 'ProgramFiles', 'ProgramFiles(x86)', 'LOCALAPPDATA', 'APPDATA',
13
+ 'DSH_HOME', 'DSH_WORKSPACE', 'DSH_SESSION_ID', 'CWD', 'WORKSPACE',
14
+ ])