dsh-plugin-prompt-tool 0.4.2 → 0.5.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 (188) hide show
  1. package/README.md +125 -222
  2. package/engine/compaction-epoch.mjs +139 -0
  3. package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
  4. package/engine/compositions/library/compaction.yml +52 -0
  5. package/engine/compositions/library/context-gate.yml +40 -0
  6. package/engine/compositions/library/custom-bash.yml +14 -0
  7. package/engine/compositions/library/delegation.yml +85 -0
  8. package/engine/compositions/library/official-agent-instructions.yml +17 -0
  9. package/engine/compositions/library/official-persistent-shell.yml +56 -0
  10. package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
  11. package/engine/compositions/library/official-tool-bash.yml +6 -0
  12. package/engine/compositions/library/official-tool-cordis.yml +13 -0
  13. package/engine/compositions/library/official-tool-presentation.yml +7 -0
  14. package/engine/compositions/library/official-tool-skill.yml +14 -0
  15. package/engine/compositions/library/persistent-shell.yml +36 -0
  16. package/engine/compositions/library/persona.yml +9 -0
  17. package/engine/compositions/library/planning.yml +41 -0
  18. package/engine/compositions/library/prompt-config-engine.yml +16 -0
  19. package/engine/compositions/library/router-first-turn.yml +9 -0
  20. package/engine/compositions/library/run-code-env.yml +15 -0
  21. package/engine/compositions/library/skill-filesystem.yml +13 -0
  22. package/engine/compositions/library/skill-search.yml +14 -0
  23. package/engine/compositions/library/str-replace-editor.yml +10 -0
  24. package/engine/compositions/library/tool-ask-user.yml +8 -0
  25. package/engine/compositions/library/tool-bash.yml +16 -0
  26. package/engine/compositions/library/tool-bootstrap.yml +16 -0
  27. package/engine/compositions/library/tool-filter.yml +12 -0
  28. package/engine/compositions/library/tool-fs-search.yml +18 -0
  29. package/engine/compositions/library/tool-fs.yml +10 -0
  30. package/engine/compositions/library/tool-goal.yml +19 -0
  31. package/engine/compositions/library/tool-jobs.yml +23 -0
  32. package/engine/compositions/library/tool-pwsh.yml +12 -0
  33. package/engine/compositions/library/tool-todo.yml +11 -0
  34. package/engine/compositions/library/tool-web.yml +9 -0
  35. package/engine/compositions/source/local/context-gate.yml +37 -0
  36. package/engine/compositions/source/local/custom-bash.yml +11 -0
  37. package/engine/compositions/source/local/prompt-config-engine.yml +13 -0
  38. package/engine/compositions/source/local/router-first-turn.yml +5 -0
  39. package/engine/compositions/source/local/run-code-env.yml +12 -0
  40. package/engine/compositions/source/local/skill-search.yml +11 -0
  41. package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
  42. package/engine/context-gate.mjs +305 -0
  43. package/{preset → engine}/custom-bash.mjs +243 -243
  44. package/engine/executor.mjs +271 -0
  45. package/engine/fillers.mjs +273 -0
  46. package/engine/layers.mjs +220 -0
  47. package/engine/prompt-config-engine.mjs +49 -0
  48. package/{preset → engine}/router-first-turn.mjs +85 -73
  49. package/engine/run-code-env.mjs +208 -0
  50. package/engine/schema.mjs +294 -0
  51. package/{preset → engine}/shared.mjs +54 -0
  52. package/engine/strategies.mjs +223 -0
  53. package/{preset → engine}/tool-bootstrap.mjs +350 -282
  54. package/engine/tool-filter.mjs +80 -0
  55. package/engine/vendor/yaml/LICENSE +13 -0
  56. package/engine/vendor/yaml/dist/compose/compose-collection.js +88 -0
  57. package/engine/vendor/yaml/dist/compose/compose-doc.js +43 -0
  58. package/engine/vendor/yaml/dist/compose/compose-node.js +109 -0
  59. package/engine/vendor/yaml/dist/compose/compose-scalar.js +86 -0
  60. package/engine/vendor/yaml/dist/compose/composer.js +219 -0
  61. package/engine/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
  62. package/engine/vendor/yaml/dist/compose/resolve-block-scalar.js +198 -0
  63. package/engine/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
  64. package/engine/vendor/yaml/dist/compose/resolve-end.js +37 -0
  65. package/engine/vendor/yaml/dist/compose/resolve-flow-collection.js +207 -0
  66. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
  67. package/engine/vendor/yaml/dist/compose/resolve-props.js +146 -0
  68. package/engine/vendor/yaml/dist/compose/util-contains-newline.js +34 -0
  69. package/engine/vendor/yaml/dist/compose/util-empty-scalar-position.js +26 -0
  70. package/engine/vendor/yaml/dist/compose/util-flow-indent-check.js +15 -0
  71. package/engine/vendor/yaml/dist/compose/util-map-includes.js +13 -0
  72. package/engine/vendor/yaml/dist/doc/Document.js +335 -0
  73. package/engine/vendor/yaml/dist/doc/anchors.js +71 -0
  74. package/engine/vendor/yaml/dist/doc/applyReviver.js +55 -0
  75. package/engine/vendor/yaml/dist/doc/createNode.js +88 -0
  76. package/engine/vendor/yaml/dist/doc/directives.js +176 -0
  77. package/engine/vendor/yaml/dist/errors.js +57 -0
  78. package/engine/vendor/yaml/dist/index.js +17 -0
  79. package/engine/vendor/yaml/dist/log.js +11 -0
  80. package/engine/vendor/yaml/dist/nodes/Alias.js +116 -0
  81. package/engine/vendor/yaml/dist/nodes/Collection.js +147 -0
  82. package/engine/vendor/yaml/dist/nodes/Node.js +38 -0
  83. package/engine/vendor/yaml/dist/nodes/Pair.js +36 -0
  84. package/engine/vendor/yaml/dist/nodes/Scalar.js +24 -0
  85. package/engine/vendor/yaml/dist/nodes/YAMLMap.js +144 -0
  86. package/engine/vendor/yaml/dist/nodes/YAMLSeq.js +113 -0
  87. package/engine/vendor/yaml/dist/nodes/addPairToJSMap.js +63 -0
  88. package/engine/vendor/yaml/dist/nodes/identity.js +36 -0
  89. package/engine/vendor/yaml/dist/nodes/toJS.js +37 -0
  90. package/engine/vendor/yaml/dist/parse/cst-scalar.js +214 -0
  91. package/engine/vendor/yaml/dist/parse/cst-stringify.js +61 -0
  92. package/engine/vendor/yaml/dist/parse/cst-visit.js +97 -0
  93. package/engine/vendor/yaml/dist/parse/cst.js +98 -0
  94. package/engine/vendor/yaml/dist/parse/lexer.js +721 -0
  95. package/engine/vendor/yaml/dist/parse/line-counter.js +39 -0
  96. package/engine/vendor/yaml/dist/parse/parser.js +975 -0
  97. package/engine/vendor/yaml/dist/public-api.js +102 -0
  98. package/engine/vendor/yaml/dist/schema/Schema.js +37 -0
  99. package/engine/vendor/yaml/dist/schema/common/map.js +17 -0
  100. package/engine/vendor/yaml/dist/schema/common/null.js +15 -0
  101. package/engine/vendor/yaml/dist/schema/common/seq.js +17 -0
  102. package/engine/vendor/yaml/dist/schema/common/string.js +14 -0
  103. package/engine/vendor/yaml/dist/schema/core/bool.js +19 -0
  104. package/engine/vendor/yaml/dist/schema/core/float.js +43 -0
  105. package/engine/vendor/yaml/dist/schema/core/int.js +38 -0
  106. package/engine/vendor/yaml/dist/schema/core/schema.js +23 -0
  107. package/engine/vendor/yaml/dist/schema/json/schema.js +62 -0
  108. package/engine/vendor/yaml/dist/schema/tags.js +96 -0
  109. package/engine/vendor/yaml/dist/schema/yaml-1.1/binary.js +58 -0
  110. package/engine/vendor/yaml/dist/schema/yaml-1.1/bool.js +26 -0
  111. package/engine/vendor/yaml/dist/schema/yaml-1.1/float.js +46 -0
  112. package/engine/vendor/yaml/dist/schema/yaml-1.1/int.js +71 -0
  113. package/engine/vendor/yaml/dist/schema/yaml-1.1/merge.js +67 -0
  114. package/engine/vendor/yaml/dist/schema/yaml-1.1/omap.js +74 -0
  115. package/engine/vendor/yaml/dist/schema/yaml-1.1/pairs.js +78 -0
  116. package/engine/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
  117. package/engine/vendor/yaml/dist/schema/yaml-1.1/set.js +93 -0
  118. package/engine/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +101 -0
  119. package/engine/vendor/yaml/dist/stringify/foldFlowLines.js +146 -0
  120. package/engine/vendor/yaml/dist/stringify/stringify.js +129 -0
  121. package/engine/vendor/yaml/dist/stringify/stringifyCollection.js +153 -0
  122. package/engine/vendor/yaml/dist/stringify/stringifyComment.js +20 -0
  123. package/engine/vendor/yaml/dist/stringify/stringifyDocument.js +85 -0
  124. package/engine/vendor/yaml/dist/stringify/stringifyNumber.js +25 -0
  125. package/engine/vendor/yaml/dist/stringify/stringifyPair.js +150 -0
  126. package/engine/vendor/yaml/dist/stringify/stringifyString.js +336 -0
  127. package/engine/vendor/yaml/dist/util.js +11 -0
  128. package/engine/vendor/yaml/dist/visit.js +233 -0
  129. package/engine/vendor/yaml/index.js +5 -0
  130. package/engine/vendor/yaml/package.json +11 -0
  131. package/lib/client.js +4421 -781
  132. package/lib/client.js.map +1 -1
  133. package/lib/index.d.mts +504 -51
  134. package/lib/index.mjs +3871 -731
  135. package/lib/preset-core.d.mts +7 -37
  136. package/lib/preset-core.mjs +44 -283
  137. package/lib/prompt-configs-BcP6miu9.d.mts +102 -0
  138. package/lib/prompt-configs-k7wuZs0C.mjs +719 -0
  139. package/package.json +104 -98
  140. package/preset/anchored/preset.yml +344 -0
  141. package/preset/creative/preset.yml +62 -0
  142. package/preset/custom/preset.yml +13 -0
  143. package/preset/liangshen/preset.yml +66 -0
  144. package/preset/minimal/preset.yml +41 -0
  145. package/preset/ptc/preset.yml +50 -0
  146. package/preset/standard/preset.yml +49 -0
  147. package/skills/manifest.json +7 -0
  148. package/skills/sandboxmod/SKILL.md +49 -49
  149. package/skills/web ui/SKILL.md +42 -0
  150. package/templates/10-pre-step.yml +28 -0
  151. package/templates/11-merged-a.yml +11 -0
  152. package/templates/13-anchor.yml +19 -0
  153. package/templates/14-first-turn-anchor.yml +27 -0
  154. package/templates/15-guide-auto.yml +25 -0
  155. package/templates/16-custom-fallback.yml +21 -0
  156. package/templates/17-instruction-hint.yml +23 -0
  157. package/templates/18-placeholder-env-facts.yml +11 -0
  158. package/templates/19-placeholder-skill-catalog.yml +19 -0
  159. package/templates/20-system-section.yml +14 -0
  160. package/templates/30-runtime-context.yml +10 -0
  161. package/templates/31-runtime-context-placeholder.yml +18 -0
  162. package/templates/40-agent-request.yml +11 -0
  163. package/templates/50-llm-stream.yml +9 -0
  164. package/templates/60-tool-pipeline.yml +12 -0
  165. package/AGENTS.md +0 -4
  166. package/plan.md +0 -312
  167. package/preset/agent.cordis.yml +0 -443
  168. package/preset/compaction-epoch.mjs +0 -81
  169. package/preset/context-gate.mjs +0 -165
  170. package/preset/instruction-hint.mjs +0 -217
  171. package/preset/near-anchor.mjs +0 -101
  172. package/preset/preset.yml +0 -3
  173. package/preset/prompt-injector.mjs +0 -112
  174. package/preset/router-guide.mjs +0 -79
  175. package/preset.md +0 -115
  176. package/upstream/dsh-anchored-standard/LICENSE +0 -22
  177. package/upstream/dsh-anchored-standard/NOTICE +0 -19
  178. package/upstream/dsh-anchored-standard/REVISION +0 -1
  179. package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
  180. package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
  181. package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
  182. package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
  183. package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
  184. package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
  185. package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
  186. package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
  187. package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
  188. /package/{preset → engine}/skill-search.mjs +0 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * executor — pre-step 消息批执行器。
3
+ * 职责:过滤(层/子代理/模型/晋升)→ resolve → 插值 → 去重/合并 → 落位插入。
4
+ * 任一提示词配置失败只跳过该提示词配置并 warnOnce,绝不让注入 bug 卡死会话。
5
+ */
6
+
7
+ import {
8
+ PROMOTE_EVENTS,
9
+ createWarnOnce,
10
+ interpolateVariables,
11
+ isDelegated,
12
+ matchesModel,
13
+ newMessageId,
14
+ } from './shared.mjs'
15
+ import { createEpochPromotion } from './compaction-epoch.mjs'
16
+ import { wireLayers } from './layers.mjs'
17
+
18
+ const name = 'prompt-config-engine'
19
+
20
+ /** 事件数据兼容两层形状:event.data 本身是消息,或 event.data.message 是消息。 */
21
+ function eventMessage(event) {
22
+ const data = event?.data
23
+ if (data === null || typeof data !== 'object') return undefined
24
+ const message = data.message !== null && typeof data.message === 'object' ? data.message : data
25
+ return message?.source !== null && typeof message?.source === 'object' ? message : undefined
26
+ }
27
+
28
+ /** 合并身份:merged 模式按位置分组(merged:<position>);separate 模式保持自身身份。 */
29
+ function mergedIdentity(config) {
30
+ if (config.mergeMode !== 'merged') return config.id
31
+ return `merged:${config.position}`
32
+ }
33
+
34
+ function hasInjected(config, session) {
35
+ const value = config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
36
+ return (Array.isArray(session.events) ? session.events : []).some((event) => {
37
+ const message = eventMessage(event)
38
+ // 双通道去重:kind(外来/第三方消息,如 context-gate 的 instruction-hint)或
39
+ // plugin(本引擎注入的命名空间,merged 组用 merged:<position>)。
40
+ return message?.source?.kind === config.sourceKind || message?.source?.plugin === value
41
+ })
42
+ }
43
+
44
+ /** 当前消息批内是否已有该提示词配置注入(每轮去重)。 */
45
+ function hasInBatch(config, messages) {
46
+ const value = config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
47
+ return messages.some((message) =>
48
+ message?.source?.kind === config.sourceKind || message?.source?.plugin === value)
49
+ }
50
+
51
+ /** 构造默认 user/assistant 消息;策略返回完整 patch 时覆盖对应字段。 */
52
+ function buildMessage(config, resolved) {
53
+ const text = typeof resolved.text === 'string' ? resolved.text : ''
54
+ const defaultContent = config.texts.length > 0
55
+ ? config.texts.map((item) => ({ type: 'text', text: item }))
56
+ : [{ type: 'text', text }]
57
+ const sourceValue = mergedIdentity(config)
58
+ return {
59
+ id: typeof resolved.id === 'string' && resolved.id.length > 0 ? resolved.id : newMessageId(config.id),
60
+ role: typeof resolved.role === 'string' ? resolved.role : config.role,
61
+ content: Array.isArray(resolved.content) ? resolved.content : defaultContent,
62
+ source: resolved.source !== null && typeof resolved.source === 'object'
63
+ ? resolved.source
64
+ : {
65
+ kind: config.sourceKind,
66
+ plugin: sourceValue,
67
+ ...(typeof config.form === 'string' ? { form: config.form } : {}),
68
+ ...(typeof config.summary === 'string' && config.summary.length > 0 ? { summary: config.summary } : {}),
69
+ },
70
+ }
71
+ }
72
+
73
+ /** 按提示词配置声明的插入位置写入消息批;找不到插入锚点返回 false(本提示词配置跳过)。 */
74
+ function insertMessage(config, messages, message) {
75
+ if (config.position === 'before-all') {
76
+ messages.unshift(message)
77
+ return true
78
+ }
79
+ if (config.position === 'after-all') {
80
+ messages.push(message)
81
+ return true
82
+ }
83
+ // 默认 after-user:只锚真实用户消息,插件消息不算。
84
+ const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
85
+ if (userIndex < 0) return false
86
+ messages.splice(userIndex + 1, 0, message)
87
+ return true
88
+ }
89
+
90
+ /**
91
+ * 把一组运行时提示词配置装配为注入执行器。
92
+ * @param prepend 是否以 prepend 注册 pre-step(合并行恒 true;单条提示词配置兼容层由参数决定)。
93
+ */
94
+ export function applyPromptConfigs(ctx, configs, options = {}) {
95
+ const list = configs.filter((config) => config !== undefined && config !== null)
96
+ if (list.length === 0) return
97
+ // 互斥组:同一 group 且 exclusive=true 时,只保留排序后第一个 enabled 提示词配置。
98
+ const claimedGroups = new Set()
99
+ const effectiveList = list.filter((config) => {
100
+ if (config.enabled === false) return false
101
+ if (config.group !== undefined && config.exclusive === true) {
102
+ if (claimedGroups.has(config.group)) return false
103
+ claimedGroups.add(config.group)
104
+ }
105
+ return true
106
+ })
107
+ const main = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
108
+ const withSubagents = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true })
109
+ ctx.on('session/event', (session, event) => {
110
+ main.observe(session, event)
111
+ withSubagents.observe(session, event)
112
+ })
113
+
114
+ /** 每提示词配置每会话的进程内快路径;真相在持久事件流。 */
115
+ // ponytail: 会话数上限防无界增长(Set 按 session.id 累积);若需精确淘汰改 LRU。
116
+ const MAX_MEMO_SESSIONS = 4096
117
+ const injectedMemo = new Map()
118
+ const configMemo = (config) => {
119
+ let memo = injectedMemo.get(config.id)
120
+ if (memo === undefined) {
121
+ if (injectedMemo.size >= MAX_MEMO_SESSIONS) injectedMemo.clear()
122
+ memo = new Set()
123
+ injectedMemo.set(config.id, memo)
124
+ }
125
+ return memo
126
+ }
127
+
128
+ const warnOnce = createWarnOnce(ctx, name)
129
+ const prepend = options.prepend === true || list.some((config) => config.prepend === true)
130
+
131
+ // 非 pre-step 提示词配置接入各自声明的官方层级通道(system-section /
132
+ // runtime-context / agent-request / llm-stream / tool-pipeline)。
133
+ wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
134
+
135
+ ctx.on('agent/pre-step', async ({ agent }, next) => {
136
+ const decision = await next()
137
+ try {
138
+ if (decision.kind === 'reject') return decision
139
+ if (agent === undefined) return decision
140
+ const session = agent.session
141
+ if (session === undefined) return decision
142
+
143
+ const messages = Array.isArray(decision.messages) ? [...decision.messages] : []
144
+ let changed = false
145
+
146
+ const due = []
147
+ for (const config of effectiveList) {
148
+ try {
149
+ if (config.layer !== 'pre-step') continue
150
+ const delegated = isDelegated(session)
151
+ if (config.audience === 'main' && delegated) continue
152
+ if (config.audience === 'subagent' && !delegated) continue
153
+ if (!matchesModel(config.modelScope, agent.options?.model)) continue
154
+ if (config.promotion === 'main' && !main.status(agent).promoted) continue
155
+ if (config.promotion === 'include-subagents' && !withSubagents.status(agent).promoted) continue
156
+
157
+ const memo = configMemo(config)
158
+ if (config.dedupe === 'session') {
159
+ if (memo.has(session.id) || hasInjected(config, session)) {
160
+ memo.add(session.id)
161
+ continue
162
+ }
163
+ } else if (config.dedupe === 'batch' && hasInBatch(config, messages)) {
164
+ continue
165
+ }
166
+
167
+ const resolved = await config.resolve({ ctx, agent, session, decision, messages })
168
+ if (resolved === null || resolved === undefined) continue
169
+ const patched = { ...resolved }
170
+ // params 并入插值变量:ST 变量(setvar/getvar 收集 + 预设参数)顶层 key 直接可插值
171
+ //(含中文 key 如 {{接受值}}——引擎正则已支持 Unicode 字母)。
172
+ const mergedVars = { ...config.variables, ...config.params, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
173
+ if (typeof patched.text === 'string') {
174
+ // 提示词配置级模板变量 + filler 变量 + 内置环境变量插值。
175
+ patched.text = interpolateVariables(patched.text, mergedVars, session)
176
+ }
177
+ if (config.texts.length > 0) {
178
+ const blocks = config.texts
179
+ .map((item) => interpolateVariables(item, mergedVars, session))
180
+ .filter((item) => item.length > 0)
181
+ .map((item) => ({ type: 'text', text: item }))
182
+ if (blocks.length > 0) patched.content = blocks
183
+ }
184
+ const hasText = typeof patched.text === 'string' && patched.text.length > 0
185
+ const hasContent = Array.isArray(patched.content) && patched.content.length > 0
186
+ if (!hasText && !hasContent) continue
187
+ due.push({ config, resolved: patched })
188
+ } catch (error) {
189
+ // 单个提示词配置失败不得伤及会话:跳过该提示词配置,只告警一次。
190
+ warnOnce(`${name}: config ${String(config?.id ?? '<unknown>')} failed, skipping: ${String((error && error.message) || error)}`)
191
+ }
192
+ }
193
+
194
+ // order 升序(同值保持声明顺序):决定拼接顺序与同位置插入顺序。
195
+ due.sort((a, b) => a.config.order - b.config.order)
196
+
197
+ // merged 模式的多条提示词配置在首条配置的位置合并为一条消息;
198
+ // 文本按内容块拼接,source 身份改用 merged:<position> 以保持持久幂等。
199
+ const orderedGroups = []
200
+ const groupIndex = new Map()
201
+ for (const entry of due) {
202
+ const group = entry.config.mergeMode === 'merged'
203
+ ? mergedIdentity(entry.config)
204
+ : undefined
205
+ if (group === undefined) {
206
+ orderedGroups.push([entry])
207
+ continue
208
+ }
209
+ let index = groupIndex.get(group)
210
+ if (index === undefined) {
211
+ index = orderedGroups.length
212
+ groupIndex.set(group, index)
213
+ orderedGroups.push([])
214
+ }
215
+ orderedGroups[index].push(entry)
216
+ }
217
+
218
+ const planned = []
219
+ for (const group of orderedGroups) {
220
+ const base = group[0]
221
+ const message = buildMessage(base.config, base.resolved)
222
+ if (group.length > 1) {
223
+ message.content = group.flatMap((entry) => {
224
+ if (Array.isArray(entry.resolved.content)) return entry.resolved.content
225
+ return typeof entry.resolved.text === 'string' && entry.resolved.text.length > 0
226
+ ? [{ type: 'text', text: entry.resolved.text }]
227
+ : []
228
+ })
229
+ if (message.source !== null && typeof message.source === 'object') {
230
+ message.source = { ...message.source, plugin: mergedIdentity(base.config) }
231
+ }
232
+ if (new Set(group.map((entry) => entry.config.position)).size > 1) {
233
+ warnOnce(`${name}: merged group mixes positions — using ${String(base.config.position)} from the first config`)
234
+ }
235
+ }
236
+ planned.push({ position: base.config.position, message, group })
237
+ }
238
+
239
+ // 同位置批量插入:planned 已按 order 升序,多元素 splice/unshift/push 保持该顺序。
240
+ const markGroup = (group) => {
241
+ for (const entry of group) {
242
+ if (entry.config.dedupe === 'session') configMemo(entry.config).add(session.id)
243
+ }
244
+ }
245
+ const beforeAll = planned.filter((item) => item.position === 'before-all')
246
+ const afterUser = planned.filter((item) => item.position !== 'before-all' && item.position !== 'after-all')
247
+ const afterAll = planned.filter((item) => item.position === 'after-all')
248
+ if (beforeAll.length > 0) {
249
+ messages.unshift(...beforeAll.map((item) => item.message))
250
+ for (const item of beforeAll) markGroup(item.group)
251
+ changed = true
252
+ }
253
+ const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
254
+ if (afterUser.length > 0 && userIndex >= 0) {
255
+ messages.splice(userIndex + 1, 0, ...afterUser.map((item) => item.message))
256
+ for (const item of afterUser) markGroup(item.group)
257
+ changed = true
258
+ }
259
+ if (afterAll.length > 0) {
260
+ messages.push(...afterAll.map((item) => item.message))
261
+ for (const item of afterAll) markGroup(item.group)
262
+ changed = true
263
+ }
264
+
265
+ return changed ? { ...decision, messages } : decision
266
+ } catch (error) {
267
+ warnOnce(`${name}: prompt config failed, skipping: ${String((error && error.message) || error)}`)
268
+ return decision
269
+ }
270
+ }, { prepend })
271
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * fillers — 引擎内置的动态填充器(strategy=placeholder 的数据源)。
3
+ * 全部遵循同一铁律:服务缺失或列表失败时返回 null(跳过该配置),绝不伤及会话。
4
+ */
5
+
6
+ import { readFileSync } from 'node:fs'
7
+ import { getService, parseToolNames } from './shared.mjs'
8
+
9
+ const name = 'prompt-config-engine'
10
+
11
+ const PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
12
+ const USER_GLOBAL_CANDIDATE = 'AGENTS.md'
13
+
14
+ /** instruction-hint 提示文本:agents-instruction.md 优先,否则动态探测。
15
+ * path 可经提示词配置 params.agentsInstructionPath 指定(共享引擎时引擎文件在
16
+ * 预设根 .engine,必须显式指向预设目录内的文件,如 ../<预设>/agents-instruction.md)。 */
17
+ function readAgentsInstructionText(path = './agents-instruction.md') {
18
+ try {
19
+ return readFileSync(new URL(path, import.meta.url), 'utf8').trim()
20
+ } catch {
21
+ return ''
22
+ }
23
+ }
24
+
25
+ /** Find the project root: first ancestor containing any root marker (e.g. .git). */
26
+ async function findProjectRoot(fs, cwd, signal) {
27
+ let current = cwd
28
+ for (;;) {
29
+ for (const marker of ['.git', '.hg', '.svn']) {
30
+ try {
31
+ const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
32
+ const info = await fs.stat(target, signal)
33
+ if (info !== undefined) return current
34
+ } catch {
35
+ // Probe failure = marker absent; continue.
36
+ }
37
+ }
38
+ const parent = parentPath(current)
39
+ if (parent === current || parent.length === 0) return cwd
40
+ current = parent
41
+ }
42
+ }
43
+
44
+ /** List instruction files present in one directory (project candidates). */
45
+ async function presentInDir(fs, dir, candidates, signal) {
46
+ const found = []
47
+ for (const candidate of candidates) {
48
+ try {
49
+ const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
50
+ const info = await fs.stat(target, signal)
51
+ if (info !== undefined && info.type === 'file') found.push(candidate)
52
+ } catch {
53
+ // Absent or unreadable — skip.
54
+ }
55
+ }
56
+ return found
57
+ }
58
+
59
+ /** Join one path segment onto a directory (platform-agnostic string join). */
60
+ function joinPath(dir, segment) {
61
+ if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
62
+ const sep = dir.includes('\\') ? '\\' : '/'
63
+ return dir + sep + segment
64
+ }
65
+
66
+ /** Parent of an absolute Windows or POSIX path. */
67
+ function parentPath(path) {
68
+ const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
69
+ if (idx <= 0) return path
70
+ const parent = path.slice(0, idx)
71
+ return parent.length === 0 ? path : parent
72
+ }
73
+
74
+ /**
75
+ * instruction-hint:晋升后一次性提示指令文件存在(agents-instruction.md 优先)。
76
+ * 可自定义参数:params.text 直接作为提示文本(覆盖文件与动态探测);
77
+ * 其余字段语义与 env-facts / skill-catalog 一致(config.params 由单一配置源下发)。
78
+ */
79
+ function createInstructionHintResolver(config) {
80
+ const customText = typeof config?.params?.text === 'string' && config.params.text.trim().length > 0
81
+ ? config.params.text.trim()
82
+ : ''
83
+ // 共享引擎:引擎文件在预设根 .engine,agents-instruction.md 必须经
84
+ // params.agentsInstructionPath 显式指向预设目录内文件(旧版无此参数时回退
85
+ // './agents-instruction.md'——引擎文件与内容资产同目录的旧布局;显式 .txt 旧路径
86
+ // 仍可读(升级前生成的组合,文件未重建前保持兼容))。
87
+ const instructionPath = typeof config?.params?.agentsInstructionPath === 'string'
88
+ && config.params.agentsInstructionPath.trim().length > 0
89
+ ? config.params.agentsInstructionPath
90
+ : './agents-instruction.md'
91
+ const agentsInstructionText = readAgentsInstructionText(instructionPath)
92
+ return async ({ ctx, agent, session }) => {
93
+ const id = `instruction-hint-${session.id}`
94
+ if (customText.length > 0) {
95
+ return {
96
+ id,
97
+ text: customText,
98
+ source: { kind: 'instruction-hint', form: 'hint' },
99
+ }
100
+ }
101
+ if (agentsInstructionText.length > 0) {
102
+ return {
103
+ id,
104
+ text: agentsInstructionText,
105
+ source: { kind: 'instruction-hint', form: 'hint' },
106
+ }
107
+ }
108
+ const fs = ctx.get('fs')
109
+ if (fs === undefined) return null
110
+ const cwd = session.header?.cwd ?? process.cwd()
111
+ const projectFiles = []
112
+ const root = await findProjectRoot(fs, cwd, agent.signal)
113
+ projectFiles.push(...await presentInDir(fs, root, PROJECT_CANDIDATES, agent.signal))
114
+ const userGlobalFiles = []
115
+ try {
116
+ const dshHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
117
+ if (dshHome !== undefined) {
118
+ userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_CANDIDATE], agent.signal))
119
+ }
120
+ } catch {
121
+ // Unreadable home probe — ignore.
122
+ }
123
+ const sections = []
124
+ if (projectFiles.length > 0) {
125
+ sections.push(`Workspace instruction files exist: ${projectFiles.join(', ')} (project root: ${root}).`)
126
+ }
127
+ if (userGlobalFiles.length > 0) {
128
+ sections.push(`A user-global instruction file exists: ${USER_GLOBAL_CANDIDATE}.`)
129
+ }
130
+ if (sections.length === 0) return null
131
+ return {
132
+ id,
133
+ text: [
134
+ ...sections,
135
+ 'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
136
+ ].join(' '),
137
+ source: { kind: 'instruction-hint', form: 'hint' },
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * env-facts:机器事实动态填充器。
144
+ * params.envKeys 逗号分隔环境变量白名单,默认 DSH_HOME,DSH_WORKSPACE;
145
+ * CWD 特殊映射到 session.header.cwd ?? process.cwd()。
146
+ * 返回 facts 变量表与默认文本;用户可用 text 模板 + {{变量}} 完全自定义输出。
147
+ */
148
+ function createEnvFactsResolver(config) {
149
+ const keys = parseToolNames(typeof config.params?.envKeys === 'string' && config.params.envKeys.length > 0
150
+ ? config.params.envKeys
151
+ : 'DSH_HOME,DSH_WORKSPACE')
152
+ return ({ agent }) => {
153
+ const session = agent?.session
154
+ const cwd = session?.header?.cwd ?? process.cwd()
155
+ const builtinHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : '')
156
+ const facts = {}
157
+ for (const key of keys) {
158
+ let value
159
+ if (key === 'CWD' || key === 'WORKSPACE') value = cwd
160
+ else if (key === 'DSH_HOME') value = builtinHome
161
+ else value = process.env[key]
162
+ if (typeof value === 'string' && value.length > 0) facts[key] = value
163
+ }
164
+ facts.WORKSPACE = process.env.DSH_WORKSPACE ?? cwd
165
+ facts.CWD = cwd
166
+ const defaultText = ['Environment facts:', ...Object.entries(facts).map(([key, value]) => `- ${key}=${value}`)].join('\n')
167
+ return { text: defaultText, variables: facts }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * skill-catalog:技能目录动态填充器。
173
+ * 数据来自宿主 skills 服务(聚合各 provider;本插件 provider 的 list 已按
174
+ * skillSwitches 过滤,因此目录天然只含已启用技能)。服务缺失或列表失败时
175
+ * 跳过本配置并告警一次,绝不伤及会话。
176
+ */
177
+ function createSkillCatalogResolver(config) {
178
+ const limit = Number.isSafeInteger(config.params?.limit) && config.params.limit >= 0
179
+ ? config.params.limit
180
+ : 20
181
+ const fields = parseToolNames(typeof config.params?.fields === 'string' ? config.params.fields : 'name,description')
182
+ .filter((field) => ['name', 'description', 'whenToUse'].includes(field))
183
+ const providers = parseToolNames(config.params?.providers)
184
+ const emptyBehavior = config.params?.emptyBehavior === 'text' ? 'text' : 'skip'
185
+ const emptyText = typeof config.params?.emptyText === 'string' && config.params.emptyText.length > 0
186
+ ? config.params.emptyText
187
+ : '当前没有可用技能。'
188
+ let warned = false
189
+ const warnOnceLocal = (ctx, message) => {
190
+ if (warned) return
191
+ warned = true
192
+ try {
193
+ ctx?.logger?.warn(message)
194
+ } catch {
195
+ // Logger unavailable — the guard exists only to avoid spamming.
196
+ }
197
+ }
198
+
199
+ return async ({ ctx, agent, session }) => {
200
+ try {
201
+ const skills = getService(ctx, 'skills')
202
+ if (skills === undefined || typeof skills.list !== 'function') {
203
+ warnOnceLocal(ctx, `${name}: skills service unavailable — skill-catalog config ${config.id} skipped`)
204
+ return null
205
+ }
206
+ const all = await skills.list({
207
+ scope: agent ?? ctx,
208
+ ...(session?.header?.cwd !== undefined ? { cwd: session.header.cwd } : {}),
209
+ ...(agent?.signal !== undefined ? { signal: agent.signal } : {}),
210
+ })
211
+ const visible = Array.isArray(all) ? all : []
212
+ const scoped = providers.length === 0
213
+ ? visible
214
+ : visible.filter((skill) => providers.includes(skill?.provider))
215
+ const total = scoped.length
216
+ if (total === 0) {
217
+ if (emptyBehavior !== 'text') return null
218
+ return { text: emptyText, variables: { SKILL_COUNT: '0', SKILL_NAMES: '', SKILLS_TEXT: '' } }
219
+ }
220
+
221
+ const nameOf = (skill) => {
222
+ const value = skill?.name
223
+ if (typeof value === 'string' && value.length > 0) return value
224
+ if (typeof skill?.locator === 'string' && skill.locator.length > 0) return skill.locator
225
+ return String(skill?.id ?? '(unnamed)')
226
+ }
227
+ const firstLine = (value) => typeof value === 'string' ? value.split('\n')[0].trim() : ''
228
+ const rows = (limit === 0 ? scoped : scoped.slice(0, limit)).map((skill) => {
229
+ const parts = []
230
+ if (fields.includes('name')) parts.push(nameOf(skill))
231
+ if (fields.includes('description')) {
232
+ const description = firstLine(skill?.description)
233
+ if (description.length > 0) parts.push(description)
234
+ }
235
+ if (fields.includes('whenToUse')) {
236
+ const whenToUse = firstLine(skill?.whenToUse)
237
+ if (whenToUse.length > 0) parts.push(`适用:${whenToUse}`)
238
+ }
239
+ return parts.length === 0 ? '' : `- ${parts.join(': ')}`
240
+ }).filter((line) => line.length > 0)
241
+ const skillsText = rows.join('\n')
242
+ const skillsNames = scoped.map((skill) => nameOf(skill)).join(', ')
243
+ return {
244
+ text: `Available skills (${total}):\n${skillsText}`,
245
+ variables: {
246
+ SKILL_COUNT: String(total),
247
+ SKILL_NAMES: skillsNames,
248
+ SKILLS_TEXT: skillsText,
249
+ },
250
+ }
251
+ } catch (error) {
252
+ warnOnceLocal(ctx, `${name}: skill-catalog config ${config.id} failed, skipping: ${String((error && error.message) || error)}`)
253
+ return null
254
+ }
255
+ }
256
+ }
257
+
258
+ /** 动态填充器:strategy=placeholder 时按 fill 键在注入点填充内容。 */
259
+ export const FILLERS = {
260
+ 'instruction-hint': (config) => createInstructionHintResolver(config),
261
+ 'env-facts': (config) => createEnvFactsResolver(config),
262
+ 'skill-catalog': (config) => createSkillCatalogResolver(config),
263
+ }
264
+
265
+ export function createPlaceholderResolver(config) {
266
+ const make = FILLERS[config.fill]
267
+ if (make === undefined) {
268
+ throw new TypeError(`${name}: unknown config fill ${JSON.stringify(config.fill)}`)
269
+ }
270
+ return make(config)
271
+ }
272
+
273
+ export { createInstructionHintResolver }