dsh-plugin-prompt-tool 2.0.1 → 3.0.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 (231) 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 +5 -7
  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 +23 -15
  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 +62 -19
  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 +4 -7
  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 +4 -4
  40. package/engine/conditions/count.mjs +43 -13
  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 +10 -5
  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 +165 -230
  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 +302 -28
  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 +47 -26
  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 +39 -35
  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 +37 -762
  123. package/lib/index.mjs +26919 -5791
  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 +158 -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 +100 -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/README.md +10 -0
  146. package/modules/soul/assets/templates/agents-template.md +6 -0
  147. package/modules/soul/assets/vital.md +9 -0
  148. package/modules/soul/module.yml +311 -0
  149. package/modules/superpowers/module.yml +122 -0
  150. package/modules/tool-surface/module.yml +51 -47
  151. package/package.json +40 -41
  152. package/skills/dsh-module/SKILL.md +119 -0
  153. package/skills/dsh-module/reference.md +185 -0
  154. package/templates/10-pre-step.yml +2 -3
  155. package/templates/14-first-turn-anchor.yml +10 -14
  156. package/templates/15-guide-auto.yml +32 -28
  157. package/templates/16-custom-fallback.yml +33 -21
  158. package/templates/20-system-section.yml +1 -4
  159. package/templates/50-llm-stream.yml +4 -10
  160. package/templates/60-tool-pipeline.yml +12 -10
  161. package/templates/63-append-context.yml +20 -0
  162. package/templates/64-pre-step-filter.yml +23 -0
  163. package/templates/65-turn-stop.yml +2 -2
  164. package/templates/67-inbox-prepend.yml +22 -0
  165. package/templates/70-subagent-maintenance.yml +9 -10
  166. package/templates/75-subagent-end.yml +2 -2
  167. package/templates/80-tool-surface.yml +41 -57
  168. package/templates/tools/40-delegate-module-import.yml +22 -0
  169. package/templates/tools/50-delegate-module-rules.yml +14 -0
  170. package/templates/tools/60-delegate-module-variables.yml +17 -0
  171. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  172. package/engine/actions/decision.mjs +0 -54
  173. package/engine/actions/guard.mjs +0 -80
  174. package/engine/actions/sdk-strip.mjs +0 -30
  175. package/engine/character-tools.mjs +0 -8
  176. package/engine/classify-task.mjs +0 -53
  177. package/engine/compositions/library/agent-instructions.yml +0 -9
  178. package/engine/compositions/library/command-goal.yml +0 -7
  179. package/engine/compositions/library/compaction.yml +0 -22
  180. package/engine/compositions/library/delegation-ptc.yml +0 -58
  181. package/engine/compositions/library/delegation.yml +0 -56
  182. package/engine/compositions/library/persistent-shell.yml +0 -50
  183. package/engine/compositions/library/planning.yml +0 -26
  184. package/engine/compositions/library/skill-filesystem-cordis.yml +0 -10
  185. package/engine/compositions/library/skill-filesystem.yml +0 -7
  186. package/engine/compositions/library/tool-ask-user.yml +0 -7
  187. package/engine/compositions/library/tool-bash.yml +0 -8
  188. package/engine/compositions/library/tool-cordis.yml +0 -7
  189. package/engine/compositions/library/tool-fs-search.yml +0 -9
  190. package/engine/compositions/library/tool-fs.yml +0 -7
  191. package/engine/compositions/library/tool-goal.yml +0 -7
  192. package/engine/compositions/library/tool-jobs.yml +0 -7
  193. package/engine/compositions/library/tool-plugin-manager-disabled.yml +0 -8
  194. package/engine/compositions/library/tool-plugin-manager.yml +0 -8
  195. package/engine/compositions/library/tool-present.yml +0 -7
  196. package/engine/compositions/library/tool-presentation.yml +0 -9
  197. package/engine/compositions/library/tool-pwsh.yml +0 -8
  198. package/engine/compositions/library/tool-skill.yml +0 -7
  199. package/engine/compositions/library/tool-todo.yml +0 -9
  200. package/engine/compositions/library/tool-web.yml +0 -10
  201. package/engine/compositions/source/local/character-tools.yml +0 -5
  202. package/engine/compositions/source/local/declared-triggers.yml +0 -12
  203. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  204. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  205. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  206. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  207. package/engine/condition.mjs +0 -96
  208. package/engine/declared-triggers.mjs +0 -93
  209. package/engine/session-var-tools.mjs +0 -8
  210. package/engine/session-vars.mjs +0 -54
  211. package/engine/st-macros.d.mts +0 -22
  212. package/engine/st-macros.mjs +0 -208
  213. package/engine/st-render.mjs +0 -109
  214. package/engine/st-world-book.d.mts +0 -10
  215. package/engine/subagent-tool-policy-core.mjs +0 -400
  216. package/engine/subagent-tool-policy.mjs +0 -352
  217. package/engine/trigger-spec.mjs +0 -173
  218. package/engine/world-book-tools.mjs +0 -8
  219. package/lib/migrate-rules.d.mts +0 -1
  220. package/lib/migrate-rules.mjs +0 -39
  221. package/lib/module-package-CHPFweqK.mjs +0 -2
  222. package/lib/module-package-Cnd1_mwx.mjs +0 -2872
  223. package/lib/paths-CyP0IkaM.d.mts +0 -1390
  224. package/lib/preset-transfer.d.mts +0 -2
  225. package/lib/preset-transfer.mjs +0 -3
  226. package/lib/rules-migration-DJ6vASB5.mjs +0 -15334
  227. package/templates/policies/legacy-defaults.yml +0 -34
  228. package/templates/tools/40-delegate-character.yml +0 -15
  229. package/templates/tools/50-delegate-worldbook.yml +0 -15
  230. package/templates/tools/60-delegate-session-var.yml +0 -15
  231. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -125,7 +125,7 @@ function parseTsMembers(lines, block) {
125
125
 
126
126
  /** 行多重集包含:`after` 的每一行都能在 `before` 里找到未用尽的同款(= 只删不增)。 */
127
127
  function linesOnlyRemoved(before, after) {
128
- // ponytail: O(行长) 计数表,够用;段文本是单次 assembly 的投影,不做字典序指纹。
128
+ // shortcut: O(行长) 计数表,够用;段文本是单次 assembly 的投影,不做字典序指纹。
129
129
  const pool = new Map()
130
130
  for (const line of before) pool.set(line, (pool.get(line) ?? 0) + 1)
131
131
  for (const line of after) {
@@ -256,7 +256,7 @@ function stripPython(lines, names) {
256
256
  }
257
257
  if (removed.size === 0) return undefined
258
258
  // 删空协议体(只剩注释)不是合法 Python,而补 pass 属于"新增":保守放弃。
259
- // ponytail: 这是刻意的保守上限,不是缺口——真要「一个工具都不给」应在名单/guard
259
+ // shortcut: 这是刻意的保守上限,不是缺口——真要「一个工具都不给」应在名单/guard
260
260
  // 层表达(执行边界),文本裁剪只做剩余呈现补救;若将来需要裁剪到空,改法是把
261
261
  // pruned 名单交回官方渲染器重生全文,而不是在这里补 pass。
262
262
  const body = lines.slice(header + 1, footer)
@@ -0,0 +1,10 @@
1
+ /** `service-names.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
2
+
3
+ /** pre-step 协调服务名:插件 provide、引擎 getService 读取,两侧共用这一个名字。 */
4
+ export const PRE_STEP_COORDINATOR_SERVICE: string
5
+
6
+ /** 发现引擎注册表服务名:宿主 root provide,adapter 行按 agent 注册,动作按 agent 解析。 */
7
+ export const DISCOVERY_SERVICE: string
8
+
9
+ /** 四个模块工具外壳的服务名:`pt-<引擎模块 id>`。前缀是本函数的内部细节。 */
10
+ export function toolServiceName(engineModuleId: string): string
@@ -0,0 +1,10 @@
1
+ /** pre-step 协调服务名:插件 provide、引擎 getService 读取,两侧共用这一个名字。 */
2
+ export const PRE_STEP_COORDINATOR_SERVICE = 'promptToolPreStep'
3
+
4
+ /** 发现引擎注册表:宿主 root provide,adapter 行按 agent 注册,动作按 agent 解析。 */
5
+ export const DISCOVERY_SERVICE = 'promptToolDiscovery'
6
+
7
+ /** 五个模块工具外壳的服务名:`pt-<引擎模块 id>`。前缀是本函数的内部细节。 */
8
+ export function toolServiceName(engineModuleId) {
9
+ return `pt-${engineModuleId}`
10
+ }
@@ -0,0 +1,21 @@
1
+ /** `shared.mjs` 的手写声明:只声明 `src/` 消费的导出。 */
2
+
3
+ /** 会话对象的结构化最小形状:引擎只读 `header.delegationDepth` 判主子身份。 */
4
+ export interface SessionLike {
5
+ header?: { delegationDepth?: number } | null
6
+ }
7
+
8
+ /** 主子身份判据:`delegationDepth > 0` 即子代理。 */
9
+ export function isDelegated(session: SessionLike | null | undefined): boolean
10
+
11
+ /** 视为晋升信号的 durable 会话事件类型,按 `promoteOn` 词表分档。 */
12
+ export const PROMOTE_EVENTS: {
13
+ 'tool-call': string[]
14
+ 'assistant-message': string[]
15
+ either: string[]
16
+ }
17
+
18
+ /** 按消息去重的有界告警出口。 */
19
+ export type WarnOnce = (message: string, key?: string) => void
20
+
21
+ export function createWarnOnce(ctx: { logger?: { warn(message: string): void } } | undefined, pluginName: string): WarnOnce
package/engine/shared.mjs CHANGED
@@ -63,17 +63,34 @@ export function validateConfig(pluginName, source, allowedKeys) {
63
63
  return config
64
64
  }
65
65
 
66
- /** One-shot logger guard shared by filters that degrade instead of throwing. */
66
+ /** 同一句告警只记一次的 key 上限(进程内、按 mount 一份),防不同故障刷屏。 */
67
+ export const WARN_ONCE_LIMIT = 64
68
+ /** 抑制提示的固定键(提示本身不占新故障的名额)。 */
69
+ const WARN_ONCE_SUPPRESSED = Symbol('warnOnceSuppressed')
70
+
71
+ /**
72
+ * 按消息去重的有界 logger 守卫:同一条消息只记一次,不同故障各记一次。
73
+ *
74
+ * `key` 缺省取整条消息(内嵌错误文本,因此不同故障不同 key、重复故障同 key)。
75
+ * 只有 logger 调用**成功之后**才登记,失败的告警不占名额;到上限时补一条抑制提示,此后静默。
76
+ * shortcut: 消息全文当键 → 同一故障的变体文本(id/数值不同)各占一个 key;等出现这种刷屏源再抽结构化键。
77
+ */
67
78
  export function createWarnOnce(ctx, pluginName) {
68
- let warned = false
69
- return (message) => {
70
- if (warned) return
71
- warned = true
79
+ const seen = new Set()
80
+ return (message, key = message) => {
81
+ if (seen.has(key)) return
82
+ const suppressed = seen.size >= WARN_ONCE_LIMIT
83
+ if (suppressed) {
84
+ if (seen.has(WARN_ONCE_SUPPRESSED)) return
85
+ message = `${pluginName}: 后续告警已抑制(已达 ${WARN_ONCE_LIMIT} 条不同告警上限)`
86
+ }
72
87
  try {
73
88
  ctx.logger.warn(message)
74
89
  } catch {
75
90
  // Logger unavailable — the guard exists only to avoid spamming.
91
+ return
76
92
  }
93
+ seen.add(suppressed ? WARN_ONCE_SUPPRESSED : key)
77
94
  }
78
95
  }
79
96
 
@@ -99,13 +116,9 @@ export function isDelegated(session) {
99
116
  return (session?.header?.delegationDepth ?? 0) > 0
100
117
  }
101
118
 
102
- /**
103
- * 读取当前 Session 的不可变事件快照。
104
- * 正式 API 是 snapshotEvents()(DSH 0.1.2-alpha.4+);宿主缺失该接口时按空日志处理,不再读取旧 events 数组。
105
- */
106
- export function sessionEvents(session) {
107
- const snapshot = session?.snapshotEvents?.()
108
- return Array.isArray(snapshot) ? snapshot : []
119
+ /** 已校验的静态受众;未声明或 null 同时覆盖主会话与子代理。 */
120
+ export function matchesAudience(declared, target) {
121
+ return declared == null || declared === target
109
122
  }
110
123
 
111
124
  /** True when a model id looks like a Flash-family model. */
@@ -120,6 +133,26 @@ export function matchesModel(scope, model) {
120
133
  return scope === 'flash' ? isFlash : !isFlash
121
134
  }
122
135
 
136
+ /**
137
+ * 零依赖的最小 JSON schema 编译器:把工具参数的字段字面量编译成宿主
138
+ * `ctx.tools.register` 需要的形状。`tool-search` 与 `skill-search` 共用这一份。
139
+ *
140
+ * `meta.items` 是数组元素的项描述——保留这一支:删掉它等于让带 items 的参数丢掉项描述
141
+ * (`tool-search` 的 `toolNames` 正是这种参数)。
142
+ */
143
+ export function toJsonSchema(spec) {
144
+ const properties = {}
145
+ const required = []
146
+ for (const [key, meta] of Object.entries(spec || {})) {
147
+ const prop = { type: meta.type }
148
+ if (meta.description) prop.description = meta.description
149
+ if (meta.items) prop.items = meta.items
150
+ properties[key] = prop
151
+ if (meta.required) required.push(key)
152
+ }
153
+ return { type: 'object', properties, required, additionalProperties: false }
154
+ }
155
+
123
156
  /** 逗号分隔的 token 列表;空 = 空数组(调用方自行决定"全部"语义)。 */
124
157
  export function parseToolNames(value) {
125
158
  if (typeof value !== 'string' || value.trim() === '') return []
@@ -151,24 +184,12 @@ export function keepDisposer(ctx, disposer, label) {
151
184
  * (进程内快路径,真相在 durable 事件流,清空仅触发一次冷扫重建)。 */
152
185
  export const MAX_TRACKED_SESSIONS = 4096
153
186
 
154
- /** 按会话 key 取 Map 条目;超限时清空后重建(防 session 数无界增长)。 */
155
- export function sessionMapGet(map, key, create) {
156
- let entry = map.get(key)
157
- if (entry === undefined) {
158
- if (map.size >= MAX_TRACKED_SESSIONS) map.clear()
159
- entry = create()
160
- map.set(key, entry)
161
- }
162
- return entry
163
- }
164
-
165
187
  /**
166
188
  * 会话态声明的统一访问接口(B4 T1 步骤一)。
167
189
  *
168
190
  * 收敛「每个模块自己维护会话态 Map、自己决定上限与清空策略、自己决定复位时机」,
169
191
  * 但**不改变任何既有策略**:键类型、淘汰策略、复位语义逐项由调用方按现状声明,
170
- * 本函数只把它们收进同一套读写入口。它**不替代** `sessionMapGet`(后者仍是
171
- * 「按 id + 超限全清」这一档的实现,本函数在 `evict` 缺省且按 id 索引时逐字复用它)。
192
+ * 本函数只把它们收进同一套读写入口。
172
193
  *
173
194
  * ## 三个必答项(正好对应现状的三处差异)
174
195
  *
@@ -199,7 +220,7 @@ export function sessionMapGet(map, key, create) {
199
220
  export function sessionState(ctx, create, { weak = false, limit = MAX_TRACKED_SESSIONS, evict = 'clear', reset } = {}) {
200
221
  const store = weak ? new WeakMap() : new Map()
201
222
  const keyOf = (session) => (weak ? session : session?.id)
202
- // ponytail: 无上限档(weak / limit: null / evict: null)与有上限档共用一条读写路径,
223
+ // shortcut: 无上限档(weak / limit: null / evict: null)与有上限档共用一条读写路径,
203
224
  // 只在上限判定处分支;未声明上限时不创建任何额外计数器。
204
225
  const finite = !weak && typeof limit === 'number' && limit > 0 && evict !== null
205
226
  /** 超限处理:`clear` 与 `oldest` 是**两档不同语义**,绝不互相降级。 */
@@ -21,10 +21,24 @@
21
21
  * dsh-tool-skill. If skills are unavailable the tools answer with a short
22
22
  * message instead of throwing.
23
23
  *
24
+ * 与 `tool-search.mjs` 同形状的三条修正(2026-10-16,技能面照搬工具面):
25
+ * 1. **打分而非 AND 过滤**:完整命中(命中全部 token)优先,其次近似命中;
26
+ * 长自然语言查询不再全灭。精确名命中最前。中文与英文同口径——中文整句
27
+ * 是一个 token,`includes` 仍是子串匹配。
28
+ * 2. **空查询列出全部(截到上限)**:`skill_load` 已有精确名时直接加载,空查询是
29
+ * 「先看看有什么」的入口——完整列表连同截断标注比分组摘要更好用。
30
+ * 3. **截断与空结果都明说**:显示条数 / 命中总数分列,并写明「空结果只说明
31
+ * 关键词没命中」——模型据此收窄查询或直接按精确名 `skill_load`,而不是
32
+ * 转去翻文件系统猜名字。
33
+ *
24
34
  * NOTE: this plugin REPLACES the `dsh-tool-skill` row in the composition —
25
35
  * the composition must NOT mount both, or the catalog injection returns.
26
36
  */
27
37
  import { importHostPackage } from './host-package.mjs'
38
+ import { toJsonSchema } from './shared.mjs'
39
+ import { currentEvents } from './history.mjs'
40
+ import { queryTokens, renderRows, rankRows, scoreRows } from './discovery.mjs'
41
+ import { DISCOVERY_SERVICE } from './service-names.mjs'
28
42
 
29
43
  const { isModelInvocable, renderSkillContent } = await importHostPackage('@deepseek-ai/dsh-skill')
30
44
 
@@ -34,78 +48,108 @@ export const name = 'skill-search'
34
48
  /** The agent, tools, and skills services must exist before these tools can register. */
35
49
  export const inject = ['agents', 'tools', 'skills']
36
50
 
51
+ /** 参数 schema 由 `shared.mjs` 的统一实现生成(与 `tool-search` 同一份)。 */
52
+
53
+ /** 把查询切成小写 token(Unicode 字母数字与下划线连字符,与 tool-search 同口径)。 */
54
+ export { queryTokens }
55
+
56
+ /**
57
+ * 对目录打分排序(纯函数,可单测):精确名命中排最前,其次按命中 token 数降序,
58
+ * 同分按名字字典序。`wanted` 为空时返回空数组——调用方据此区分「没给查询」与「没搜到」。
59
+ *
60
+ * 与工具面同一口径(any-token 打分,不做 AND 过滤):长自然语言查询在 AND 下命中零个,
61
+ * 等于白搜;噪声由分数排序与 MAX_RESULTS 截断控制(2026-10-22 用户指出工具面无此问题)。
62
+ */
63
+ export function scoreSkills(skills, wanted) {
64
+ return rankRows(scoreRows(skills, wanted, {
65
+ nameOf: (skill) => skill?.name,
66
+ haystackOf: (skill) => `${skill?.name ?? ''} ${skill?.description ?? ''} ${skill?.whenToUse ?? ''}`,
67
+ })).map((row) => ({ ...row, skill: row.item }))
68
+ }
69
+
70
+ /** 搜索结果上限:再多就该让模型收窄关键词。 */
37
71
  const MAX_RESULTS = 20
38
72
 
39
- /** Minimal JSON schema compiler for tool parameters (zero dependencies). */
40
- function toJsonSchema(spec) {
41
- const properties = {}
42
- const required = []
43
- for (const [key, meta] of Object.entries(spec || {})) {
44
- const prop = { type: meta.type }
45
- if (meta.description) prop.description = meta.description
46
- properties[key] = prop
47
- if (meta.required) required.push(key)
48
- }
49
- return { type: 'object', properties, required, additionalProperties: false }
73
+ /** 把目录搜索结果渲染成给模型的文本(带截断提示与加载引导)。 */
74
+ export function renderMatches(scored, query, complete = true) {
75
+ return renderRows(scored, {
76
+ query,
77
+ limit: MAX_RESULTS,
78
+ noun: 'skills',
79
+ emptyText: complete
80
+ ? `No skills match "${query}" — no skill matched your keywords; skill_load still loads any exact name in the registry, and calling skill_search with no arguments lists every visible skill.`
81
+ : `Skill discovery did not complete, so nothing can be concluded about "${query}". Retry skill_search before treating a skill as unavailable.`,
82
+ lineOf: ({ skill, name, exact }) => `- ${name}${exact ? ' (exact)' : ''}: ${String(skill?.description ?? '').split('\n')[0]}`,
83
+ warnings: complete ? [] : ['Skill discovery is incomplete; these results may be partial.'],
84
+ footer: 'Load one with skill_load({"name": "<exact name>"}).',
85
+ })
50
86
  }
51
87
 
52
- /** Register the two on-demand skill tools. */
88
+ /**
89
+ * 该技能是否已在本会话的**当前可见上下文**里注入过——去重按 `currentEvents()` 口径:
90
+ * 压缩/位置替换遮蔽掉的注入不算「已加载」,下次 `skill_load` 仍会重新注入正文。
91
+ * 官方 `/名字` 手势与 `skill_load` 写的是同一形状的 `skill-invocation` 事件,因此一并覆盖。
92
+ */
93
+ export function skillLoadedInContext(session, name) {
94
+ return currentEvents(session).some(event => event?.type === 'user/message'
95
+ && event.data?.source?.kind === 'skill-invocation'
96
+ && event.data.source.name === name)
97
+ }
98
+
99
+ /** 注册两个按需技能工具。 */
53
100
  /**
54
101
  * 本模块**刻意保持单参形态**:它不读任何 config 键,因此没有白名单可声明。
55
102
  * B2 T4 要求「四个无白名单插件行补字段声明与校验」,本行按 PLAN 的例外处理——
56
- * 若将来需要 config,先确认组合行与调用方确实都在传 config,再补 defineConfig 声明。
103
+ * 若将来需要 config,先确认组合行与调用方确实在传 config,再补 defineConfig 声明。
57
104
  */
58
- export function apply(ctx) {
59
- /** Normalize a query into lowercase tokens for simple substring matching. */
60
- const tokens = (text) => (text || '').toLowerCase().split(/[^\p{L}\p{N}_-]+/u).filter(Boolean)
61
-
62
- ctx.tools.register({
105
+ export function apply(ctx, _config, options = {}) {
106
+ const disposeSearch = ctx.tools.register({
63
107
  name: 'skill_search',
64
- description: 'Search the available skills by keyword and return matching skill names with short descriptions. This session keeps NO skill catalog in the prompt — if a task looks like it matches a skill (document conversion, image processing, game reviews, markdown, PDF, spreadsheets, …), call skill_search FIRST to find it, then skill_load to activate it. Do NOT assume skill names from memory.',
108
+ description: [
109
+ 'Search the available skills by keyword and return matching skill names with short descriptions.',
110
+ '',
111
+ 'skill_load loads any exact name in the registry, so a skill the search did not surface still loads by its name.',
112
+ '',
113
+ 'Usage:',
114
+ '- `query` — search skill names and descriptions (Chinese keywords work).',
115
+ '- call with NO arguments — list every visible skill (truncation is marked with the matching total).',
116
+ ].join('\n'),
65
117
  parameters: toJsonSchema({
66
- query: { type: 'string', required: true, description: 'search keywords (e.g. "pdf", "obsidian", "game review")' },
118
+ query: { type: 'string', required: false, description: 'search keywords (e.g. "pdf", "obsidian", "代码审查")' },
67
119
  }),
68
120
  output: {
69
121
  schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
70
122
  render: (_a, v) => [{ type: 'text', text: v.text }],
71
123
  },
72
124
  async execute(args, exec) {
73
- const wanted = tokens(args.query)
74
- const scope = exec?.agent ?? ctx
125
+ const query = typeof args?.query === 'string' ? args.query.trim() : ''
75
126
  try {
127
+ // 执行中的 agent 就是观察作用域(同 tool-search):preset 层注册的技能不带
128
+ // scope 时看不到;cwd 决定项目级技能根,两个入口共用同一组查询参数。
76
129
  const snapshot = await ctx.skills.snapshot({
77
- scope,
130
+ scope: exec?.agent ?? ctx,
78
131
  cwd: exec?.agent?.session?.header?.cwd,
79
132
  signal: exec?.signal,
80
133
  })
81
134
  exec?.signal?.throwIfAborted()
82
- const matches = snapshot.skills.filter(isModelInvocable).filter((skill) => {
83
- if (wanted.length === 0) return true
84
- const haystack = tokens(`${skill.name} ${skill.description ?? ''} ${skill.whenToUse ?? ''}`).join(' ')
85
- return wanted.every((token) => haystack.includes(token))
86
- })
87
- const head = matches.slice(0, MAX_RESULTS)
88
- const lines = head.map((skill) => {
89
- const desc = (skill.description || '').split('\n')[0]
90
- return `- ${skill.name}: ${desc}`
91
- })
92
- if (lines.length === 0) return { text: snapshot.complete
93
- ? `No skills match "${args.query}". Use skill_search with other keywords.`
94
- : 'Skill discovery is incomplete. Retry skill_search before concluding no skills match.' }
95
- const extra = matches.length > MAX_RESULTS ? `\n…(${matches.length - MAX_RESULTS} more)` : ''
96
- const incomplete = snapshot.complete ? '' : '\nSkill discovery is incomplete; these results may be partial.'
97
- return { text: `Matching skills (${matches.length}):\n${lines.join('\n')}${extra}${incomplete}\n\nLoad one with skill_load (exact name).` }
135
+ const visible = snapshot.skills.filter(isModelInvocable)
136
+ // 空查询列出可见技能(`scoreSkills` 按契约对空 token 返回 `[]`,不能代表「列出全部」);
137
+ // 不完整读只作标注、不作否决,避免把「没读全」变成「搜不到」。
138
+ const scored = query.length === 0
139
+ ? visible.map((skill) => ({ skill, name: skill.name, score: 0, exact: false }))
140
+ : scoreSkills(visible, queryTokens(query))
141
+ return { text: renderMatches(scored, query, snapshot.complete).join('\n') }
98
142
  } catch (error) {
99
143
  return { text: `skill_search unavailable: ${String((error && error.message) || error)}` }
100
144
  }
101
145
  },
102
146
  })
103
147
 
104
- ctx.tools.register({
148
+ const disposeLoad = ctx.tools.register({
105
149
  name: 'skill_load',
106
- description: 'Load the full instructions of ONE skill by its exact name (from skill_search results) and inject them for the next request. Call this before acting on a task that matches the skill.',
150
+ description: 'Load the full instructions of ONE skill by its exact registered name and inject them for the next request. Call this before acting on a task that matches the skill. The name may come from skill_search, from the skill list, or from the user — any exact name in the registry works, and when nothing matches, ask the user for it.',
107
151
  parameters: toJsonSchema({
108
- name: { type: 'string', required: true, description: 'exact skill name (kebab-case, from skill_search)' },
152
+ name: { type: 'string', required: true, description: 'exact skill name (kebab-case, as registered)' },
109
153
  }),
110
154
  output: {
111
155
  schema: { type: 'object', additionalProperties: false, properties: { text: { type: 'string' } }, required: ['text'] },
@@ -115,6 +159,13 @@ export function apply(ctx) {
115
159
  try {
116
160
  const agent = exec?.agent
117
161
  if (agent === undefined) return { text: 'skill_load requires an agent context.' }
162
+ // 名字归一:非字符串与去空白后为空都算「没给名字」,与 tool_search 的
163
+ // unlockNames 同口径。否则空串会落进「未注册」文案,调用方分不清是输入
164
+ // 非法还是名字拼错——这两件事该做的事不同。
165
+ const name = typeof args?.name === 'string' ? args.name.trim() : ''
166
+ if (name.length === 0) {
167
+ return { text: 'skill_load needs an exact skill name — call it with one, or run skill_search with no arguments to list every visible skill.' }
168
+ }
118
169
  const options = {
119
170
  scope: agent,
120
171
  cwd: agent.session.header.cwd,
@@ -122,34 +173,55 @@ export function apply(ctx) {
122
173
  }
123
174
  const snapshot = await ctx.skills.snapshot(options)
124
175
  options.signal?.throwIfAborted()
125
- const summary = snapshot.skills.find((entry) => entry.name === args.name)
176
+ const summary = snapshot.skills.find((entry) => entry.name === name)
126
177
  if (summary === undefined) {
178
+ // 名字错、发现没跑完、调用面缺 agent——三种成因的应对不同。
127
179
  return { text: snapshot.complete
128
- ? `No skill named "${args.name}". Run skill_search to list available skills.`
129
- : 'Skill discovery is incomplete. Retry skill_search before loading this skill.' }
180
+ ? `No skill named "${name}" is registered for this session. Check the exact name against what skill_search lists, or ask the user for it.`
181
+ : 'Skill discovery is incomplete, so the name could not be confirmed. Retry skill_load before concluding the skill does not exist.' }
130
182
  }
131
- if (!isModelInvocable(summary)) return { text: `Skill "${args.name}" is not available for model invocation.` }
132
- const skill = await ctx.skills.get(args.name, options)
183
+ if (!isModelInvocable(summary)) return { text: `Skill "${name}" is not available for model invocation.` }
184
+ const skill = await ctx.skills.get(name, options)
133
185
  options.signal?.throwIfAborted()
134
186
  if (skill === undefined) {
135
- return { text: `Skill "${args.name}" is no longer available. Run skill_search again.` }
187
+ return { text: `Skill "${name}" is no longer available — it was registered but its body could not be loaded. Retry once, then run skill_search to see what is still registered.` }
188
+ }
189
+ // 去重:正文已在当前可见上下文里就不再注入(少一份重复 token)。
190
+ // 只有「已加载且仍可见」才跳过——压缩遮蔽掉的注入不算已加载,见 skillLoadedInContext。
191
+ // shortcut: 同一批次内连调两次 `skill_load` 时,第一次的注入尚未落入会话,去重看不到,
192
+ // 会重复注入一份;跨批次重复才是常态,等真出现同批重复再按 step 消息判重。
193
+ if (skillLoadedInContext(agent.session, name)) {
194
+ return { text: `Skill "${name}" is already loaded in this context — its instructions are above (or earlier in this session), so nothing was injected again. Follow them without another load; if the user explicitly asks to reload it, let them re-send it as a /${name} gesture.` }
136
195
  }
137
- if (!isModelInvocable(skill)) return { text: `Skill "${args.name}" is not available for model invocation.` }
196
+ if (!isModelInvocable(skill)) return { text: `Skill "${name}" is not available for model invocation.` }
138
197
  if (skill.content.trim().length === 0) {
139
- return { text: `Skill "${args.name}" has no loadable body.` }
198
+ return { text: `Skill "${name}" has no loadable body.` }
140
199
  }
141
200
  // Queue the skill content as a non-waking next-step context message,
142
201
  // exactly like dsh-tool-skill's invocation injection.
143
202
  agent.inject({
144
- id: `skill-load-${args.name}-${Date.now()}`,
203
+ id: `skill-load-${name}-${Date.now()}`,
145
204
  role: 'user',
146
205
  content: [{ type: 'text', text: renderSkillContent(skill) }],
147
- source: { kind: 'skill-invocation', name: args.name, form: 'instructions' },
206
+ source: { kind: 'skill-invocation', name, form: 'instructions' },
148
207
  })
149
- return { text: `Skill "${args.name}" loaded; its instructions will be injected for the next request.` }
208
+ return { text: `Skill "${name}" loaded; its instructions will be injected for the next request.` }
150
209
  } catch (error) {
151
210
  return { text: `skill_load failed: ${String((error && error.message) || error)}` }
152
211
  }
153
212
  },
154
213
  })
214
+ const registry = typeof ctx.get === 'function' ? ctx.get(DISCOVERY_SERVICE) : undefined
215
+ const disposeAdapter = registry === undefined ? undefined : registry.register(options.agent, 'skills', discoveryAdapter)
216
+ return () => {
217
+ if (typeof disposeAdapter === 'function') disposeAdapter()
218
+ if (typeof disposeSearch === 'function') disposeSearch()
219
+ if (typeof disposeLoad === 'function') disposeLoad()
220
+ }
155
221
  }
222
+
223
+ /** 本引擎向发现注册表提供的元数据:技能面没有「解锁名单」,只登记工具身份。 */
224
+ export const discoveryAdapter = Object.freeze({
225
+ kind: 'skills',
226
+ tool: 'skill_search',
227
+ })
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `source-kind.mjs` 的手写声明:只声明 `src/` 消费的导出,形状按实现写实。
3
+ */
4
+
5
+ /** 拼接 v4 生产者身份。 */
6
+ export declare const pluginKind: (owner: string) => string
7
+
8
+ /** 从 v4 `plugin:<owner>` 解析 owner;裸 `plugin` 与其他形态返回 undefined。 */
9
+ export declare const pluginOwner: (kind: unknown) => string | undefined
@@ -0,0 +1,16 @@
1
+ /**
2
+ * v4 消息来源的生产者身份:`plugin:<owner>`。
3
+ * 前缀拼接、owner 解析与「是否插件来源」只在这里解释;旧日志的裸 `plugin` 只在
4
+ * `isPluginKind` 兼容,不进入 `pluginOwner`。
5
+ */
6
+ const PLUGIN_KIND_PREFIX = 'plugin:'
7
+
8
+ export const pluginKind = owner => `${PLUGIN_KIND_PREFIX}${owner}`
9
+
10
+ export const pluginOwner = kind => typeof kind === 'string' && kind.startsWith(PLUGIN_KIND_PREFIX)
11
+ ? kind.slice(PLUGIN_KIND_PREFIX.length)
12
+ : undefined
13
+
14
+ /** 本插件的 kind:v4 `plugin:<owner>`,或旧日志的裸 `plugin`(不是合法 v4 kind)。 */
15
+ export const isPluginKind = kind => kind === 'plugin'
16
+ || (typeof kind === 'string' && kind.startsWith(PLUGIN_KIND_PREFIX))
@@ -0,0 +1,5 @@
1
+ /** rule facts 与 schema 共用的 context-activation strategy id;纯 ESM,不 import 宿主或引擎模块。 */
2
+ export const CONTEXT_ACTIVATION_STRATEGY = 'context-activation'
3
+
4
+ // shortcut: context-activation 的其他字面量仍在 context-activation/actions/宿主导入路径;
5
+ // 这些路径未在本轮统一,等它们也要共享这个 id 时再收。
@@ -0,0 +1,16 @@
1
+ export interface TemplateSnapshot {
2
+ module?: Readonly<Record<string, string>>
3
+ global?: Readonly<Record<string, string>>
4
+ session?: unknown
5
+ sourceId?: string
6
+ readOnly?: boolean
7
+ samples?: Map<string, string>
8
+ }
9
+ export interface TemplateResult {
10
+ text: string
11
+ reads: Array<'global' | 'module'>
12
+ changes?: { scope: 'global' | 'module'; values: Record<string, string> }
13
+ }
14
+ export function evaluateTemplate(text: string, options?: TemplateSnapshot): TemplateResult
15
+ export function transformTemplate(text: string, macro: (body: string, expand: (value: string) => string, occurrence: number) => string): string
16
+ export function splitTemplateArgument(text: string): [string, string | undefined]