dsh-plugin-prompt-tool 0.8.1 → 2.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 (139) hide show
  1. package/README.md +84 -67
  2. package/engine/actions/append-context.mjs +49 -0
  3. package/engine/actions/assembly.mjs +150 -0
  4. package/engine/actions/catalog.mjs +92 -0
  5. package/engine/actions/content.mjs +205 -0
  6. package/engine/actions/decision.mjs +54 -0
  7. package/engine/actions/examples.mjs +11 -0
  8. package/engine/actions/guard.mjs +80 -0
  9. package/engine/actions/inbox-prepend.mjs +37 -0
  10. package/engine/actions/index.mjs +100 -0
  11. package/engine/actions/inject-text.mjs +22 -0
  12. package/engine/actions/pre-step-filter.mjs +55 -0
  13. package/engine/actions/registration.mjs +168 -0
  14. package/engine/actions/request-params.mjs +32 -0
  15. package/engine/actions/sdk-strip.mjs +30 -0
  16. package/engine/actions/shared.mjs +71 -0
  17. package/engine/actions.mjs +2 -931
  18. package/engine/branch.mjs +44 -0
  19. package/engine/character-tools.mjs +3 -6
  20. package/engine/compaction-epoch.mjs +31 -13
  21. package/engine/compositions/library/agent-instructions.yml +1 -1
  22. package/engine/compositions/library/command-goal.yml +1 -1
  23. package/engine/compositions/library/compaction.yml +1 -1
  24. package/engine/compositions/library/delegation-ptc.yml +1 -1
  25. package/engine/compositions/library/delegation.yml +1 -1
  26. package/engine/compositions/library/persistent-shell.yml +1 -1
  27. package/engine/compositions/library/planning.yml +1 -1
  28. package/engine/compositions/library/skill-filesystem-cordis.yml +1 -1
  29. package/engine/compositions/library/skill-filesystem.yml +1 -1
  30. package/engine/compositions/library/tool-ask-user.yml +1 -1
  31. package/engine/compositions/library/tool-bash.yml +1 -1
  32. package/engine/compositions/library/tool-cordis.yml +1 -1
  33. package/engine/compositions/library/tool-fs-search.yml +1 -1
  34. package/engine/compositions/library/tool-fs.yml +1 -1
  35. package/engine/compositions/library/tool-goal.yml +1 -1
  36. package/engine/compositions/library/tool-jobs.yml +1 -1
  37. package/engine/compositions/library/tool-plugin-manager-disabled.yml +1 -1
  38. package/engine/compositions/library/tool-plugin-manager.yml +1 -1
  39. package/engine/compositions/library/tool-present.yml +1 -1
  40. package/engine/compositions/library/tool-presentation.yml +1 -1
  41. package/engine/compositions/library/tool-pwsh.yml +1 -1
  42. package/engine/compositions/library/tool-skill.yml +1 -1
  43. package/engine/compositions/library/tool-todo.yml +1 -1
  44. package/engine/compositions/library/tool-web.yml +1 -1
  45. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  46. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  47. package/engine/compositions/source/local/rule-engine.yml +6 -0
  48. package/engine/condition.mjs +16 -3
  49. package/engine/conditions/anchor.mjs +32 -0
  50. package/engine/conditions/availability.mjs +47 -0
  51. package/engine/conditions/composite.mjs +83 -0
  52. package/engine/conditions/count.mjs +140 -0
  53. package/engine/conditions/evaluation.mjs +41 -0
  54. package/engine/conditions/examples.mjs +11 -0
  55. package/engine/conditions/index.mjs +85 -0
  56. package/engine/conditions/names.mjs +21 -0
  57. package/engine/conditions/phase.mjs +54 -0
  58. package/engine/conditions/preset.mjs +94 -0
  59. package/engine/conditions/scope.mjs +18 -0
  60. package/engine/conditions/session.mjs +22 -0
  61. package/engine/conditions/source.mjs +63 -0
  62. package/engine/conditions/subject.mjs +121 -0
  63. package/engine/conditions/text.mjs +44 -0
  64. package/engine/conditions/values.mjs +32 -0
  65. package/engine/dev-tool-search.mjs +213 -0
  66. package/engine/executor.mjs +93 -31
  67. package/engine/fillers.mjs +23 -16
  68. package/engine/instruction-hint.mjs +39 -23
  69. package/engine/layers.mjs +171 -77
  70. package/engine/order.mjs +20 -0
  71. package/engine/predicates.mjs +2 -753
  72. package/engine/rule-engine.mjs +40 -0
  73. package/engine/rule-runtime.mjs +110 -0
  74. package/engine/rule-spec.mjs +187 -0
  75. package/engine/schema.mjs +32 -11
  76. package/engine/session-var-tools.mjs +3 -6
  77. package/engine/shared.mjs +1 -1
  78. package/engine/st-render.mjs +8 -4
  79. package/engine/strategies.mjs +2 -235
  80. package/engine/subagent-tool-policy-core.mjs +7 -7
  81. package/engine/subagent-tool-policy.mjs +45 -20
  82. package/engine/tool-config-engine.mjs +53 -32
  83. package/engine/trigger-editor-meta.mjs +2 -21
  84. package/engine/trigger-spec.mjs +49 -133
  85. package/engine/world-book-tools.mjs +3 -6
  86. package/lib/client.js +10444 -10886
  87. package/lib/client.js.map +1 -1
  88. package/lib/index.d.mts +265 -361
  89. package/lib/index.mjs +1559 -2026
  90. package/lib/migrate-rules.d.mts +1 -0
  91. package/lib/migrate-rules.mjs +39 -0
  92. package/lib/module-package-CHPFweqK.mjs +2 -0
  93. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  94. package/lib/{paths-CY5psBqE.d.mts → paths-CyP0IkaM.d.mts} +388 -231
  95. package/lib/preset-transfer.d.mts +2 -2
  96. package/lib/preset-transfer.mjs +3 -2
  97. package/lib/{preset-package-BnJqrU4R.mjs → rules-migration-DJ6vASB5.mjs} +13825 -15283
  98. package/modules/ponytail/module.yml +406 -0
  99. package/modules/skill-surface/module.yml +74 -0
  100. package/modules/tool-surface/module.yml +67 -0
  101. package/package.json +50 -80
  102. package/templates/10-pre-step.yml +26 -22
  103. package/templates/14-first-turn-anchor.yml +25 -21
  104. package/templates/15-guide-auto.yml +34 -19
  105. package/templates/16-custom-fallback.yml +27 -15
  106. package/templates/18-placeholder-env-facts.yml +23 -14
  107. package/templates/20-system-section.yml +20 -11
  108. package/templates/30-runtime-context.yml +13 -7
  109. package/templates/40-agent-request.yml +11 -8
  110. package/templates/50-llm-stream.yml +14 -6
  111. package/templates/60-tool-pipeline.yml +15 -9
  112. package/templates/65-turn-stop.yml +19 -12
  113. package/templates/66-subagent-start.yml +12 -7
  114. package/templates/70-subagent-maintenance.yml +35 -29
  115. package/templates/75-subagent-end.yml +10 -5
  116. package/templates/80-tool-surface.yml +66 -0
  117. package/templates/policies/legacy-defaults.yml +34 -0
  118. package/templates/tools/50-delegate-worldbook.yml +1 -1
  119. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  120. package/engine/prompt-config-engine.mjs +0 -104
  121. package/preset/pt-cordis/preset.yml +0 -43
  122. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +0 -29
  123. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +0 -501
  124. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +0 -38
  125. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +0 -60
  126. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +0 -5
  127. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +0 -13
  128. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +0 -5
  129. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +0 -22
  130. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +0 -3
  131. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +0 -2
  132. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +0 -15
  133. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +0 -8
  134. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +0 -7
  135. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +0 -84
  136. package/preset/pt-custom/preset.yml +0 -15
  137. package/preset/pt-minimal/preset.yml +0 -27
  138. package/preset/pt-ptc/preset.yml +0 -45
  139. package/preset/pt-standard/preset.yml +0 -43
@@ -8,13 +8,20 @@ import { createInstructionHintResolver } from './instruction-hint.mjs'
8
8
 
9
9
  const name = 'prompt-config-engine'
10
10
 
11
+ function explicitTemplate(config) {
12
+ const texts = Array.isArray(config.texts) ? config.texts : typeof config.text === 'string' ? [config.text] : []
13
+ return texts.length > 0 ? texts.join('\n\n') : typeof config.params?.text === 'string' ? config.params.text : ''
14
+ }
15
+
11
16
  /**
12
17
  * env-facts:机器事实动态填充器。
13
18
  * params.envKeys 逗号分隔环境变量白名单:取值归模板/预设,留空 = 不附加宿主环境
14
19
  * 变量(CWD/WORKSPACE 仍由会话事实提供)。
15
- * 返回 facts 变量表与默认文本;用户可用 text 模板 + {{变量}} 完全自定义输出。
20
+ * 只提供 facts 变量;正文必须由 text/params.text 显式模板声明。
16
21
  */
17
22
  function createEnvFactsResolver(config) {
23
+ const template = explicitTemplate(config)
24
+ if (template.trim().length === 0) return () => null
18
25
  const keys = parseToolNames(typeof config.params?.envKeys === 'string' ? config.params.envKeys : '')
19
26
  return ({ agent }) => {
20
27
  const session = agent?.session
@@ -30,8 +37,8 @@ function createEnvFactsResolver(config) {
30
37
  }
31
38
  facts.WORKSPACE = process.env.DSH_WORKSPACE ?? cwd
32
39
  facts.CWD = cwd
33
- const defaultText = ['Environment facts:', ...Object.entries(facts).map(([key, value]) => `- ${key}=${value}`)].join('\n')
34
- return { text: defaultText, variables: facts }
40
+ const envFacts = Object.entries(facts).map(([key, value]) => `- ${key}=${value}`).join('\n')
41
+ return { text: template, variables: { ...facts, ENV_FACTS: envFacts } }
35
42
  }
36
43
  }
37
44
 
@@ -42,21 +49,21 @@ function createEnvFactsResolver(config) {
42
49
  * 跳过本配置并告警一次,绝不伤及会话。
43
50
  */
44
51
  function createSkillCatalogResolver(config) {
45
- const limit = Number.isSafeInteger(config.params?.limit) && config.params.limit >= 0
46
- ? config.params.limit
47
- : 20
48
- const fields = parseToolNames(typeof config.params?.fields === 'string' ? config.params.fields : 'name,description')
52
+ const template = explicitTemplate(config)
53
+ if (template.trim().length === 0) return () => null
54
+ const rawLimit = config.params?.limit
55
+ const limit = rawLimit == null || rawLimit === '' ? undefined : rawLimit
56
+ if (limit !== undefined && (!Number.isSafeInteger(limit) || limit < 0)) throw new TypeError(`${name}: skill-catalog limit must be a non-negative integer`)
57
+ const fields = parseToolNames(typeof config.params?.fields === 'string' ? config.params.fields : '')
49
58
  .filter((field) => ['name', 'description', 'whenToUse'].includes(field))
59
+ const whenToUseLabel = typeof config.params?.whenToUseLabel === 'string' ? config.params.whenToUseLabel : ''
60
+ const unnamedLabel = typeof config.params?.unnamedLabel === 'string' ? config.params.unnamedLabel : ''
50
61
  const providers = parseToolNames(config.params?.providers)
51
62
  const emptyBehavior = config.params?.emptyBehavior === 'text' ? 'text' : 'skip'
52
63
  // 空结果提示文案归模板/预设:留空且 emptyBehavior=text 时按 skip 处理(不注入空消息)。
53
64
  const emptyText = typeof config.params?.emptyText === 'string' ? config.params.emptyText : ''
54
65
  let warned = false
55
- // 保留本地 warnOnce:本工厂只拿得到 config,ctx 要到每次 resolve 调用时才由
56
- // 参数传入;换成 shared 的 createWarnOnce(ctx, name) 必须改工厂签名,而唯一
57
- // 调用点 engine/strategies.mjs 的 createPlaceholderResolver(config) 不在本支
58
- // 写区。为替换而扩大改动面违反 PLAN,故此处按原语义手写(一次性 + logger
59
- // 不可用时不抛)。
66
+ // ctx 直到 resolve 时才可用;每个填充器实例只告警一次,不让日志失败伤及会话。
60
67
  const warnOnceLocal = (ctx, message) => {
61
68
  if (warned) return
62
69
  warned = true
@@ -93,10 +100,10 @@ function createSkillCatalogResolver(config) {
93
100
  const value = skill?.name
94
101
  if (typeof value === 'string' && value.length > 0) return value
95
102
  if (typeof skill?.locator === 'string' && skill.locator.length > 0) return skill.locator
96
- return String(skill?.id ?? '(unnamed)')
103
+ return String(skill?.id ?? unnamedLabel)
97
104
  }
98
105
  const firstLine = (value) => typeof value === 'string' ? value.split('\n')[0].trim() : ''
99
- const rows = (limit === 0 ? scoped : scoped.slice(0, limit)).map((skill) => {
106
+ const rows = (limit === undefined || limit === 0 ? scoped : scoped.slice(0, limit)).map((skill) => {
100
107
  const parts = []
101
108
  if (fields.includes('name')) parts.push(nameOf(skill))
102
109
  if (fields.includes('description')) {
@@ -105,14 +112,14 @@ function createSkillCatalogResolver(config) {
105
112
  }
106
113
  if (fields.includes('whenToUse')) {
107
114
  const whenToUse = firstLine(skill?.whenToUse)
108
- if (whenToUse.length > 0) parts.push(`适用:${whenToUse}`)
115
+ if (whenToUse.length > 0) parts.push(`${whenToUseLabel}${whenToUse}`)
109
116
  }
110
117
  return parts.length === 0 ? '' : `- ${parts.join(': ')}`
111
118
  }).filter((line) => line.length > 0)
112
119
  const skillsText = rows.join('\n')
113
120
  const skillsNames = scoped.map((skill) => nameOf(skill)).join(', ')
114
121
  return {
115
- text: `Available skills (${total}):\n${skillsText}`,
122
+ text: template,
116
123
  variables: {
117
124
  SKILL_COUNT: String(total),
118
125
  SKILL_NAMES: skillsNames,
@@ -14,12 +14,15 @@
14
14
  * - `enabled`: boolean,缺省 false(组合源须显式写 true)。
15
15
  * - `promoteOn`: 'either'(缺省)| 'tool-call' | 'assistant-message'。
16
16
  * - `includeSubagents`: boolean,缺省 false(子代理首次请求即视为已晋升)。
17
+ * - `messageTemplate` / `suffixTemplate`: 显式转换模板;正文模板为空时不注册转换。
18
+ * - `projectTemplate` / `globalTemplate`: 显式文件探测提示模板,缺省均为空。
17
19
  */
18
20
 
19
21
  import { randomUUID } from 'node:crypto'
20
22
  import { readFileSync } from 'node:fs'
21
23
  import { createEpochPromotion } from './compaction-epoch.mjs'
22
- import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
24
+ import { interpolateStatic } from './interpolate.mjs'
25
+ import { booleanOption, createWarnOnce, getService, parsePromoteOn, validateConfig } from './shared.mjs'
23
26
 
24
27
  export const name = 'instruction-hint'
25
28
 
@@ -29,7 +32,7 @@ export const inject = []
29
32
  /** 项目目录链中按优先级探测的指令文件名。 */
30
33
  export const PROJECT_INSTRUCTION_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
31
34
  export const USER_GLOBAL_INSTRUCTION_CANDIDATE = 'AGENTS.md'
32
- const REFERENCE_HINT_SUFFIX = "They are reference documents about the user's environment and workspace conventions, not task instructions. Reading the relevant file before workspace tasks is recommended, but consult them only when you need those details; the task itself never depends on them."
35
+ const templateOf = (templates, key) => typeof templates?.[key] === 'string' ? templates[key] : ''
33
36
 
34
37
  /** Join one path segment onto a directory (platform-agnostic string join). */
35
38
  export function joinPath(dir, segment) {
@@ -108,22 +111,25 @@ export async function collectInstructionFiles(fs, cwd, signal, home) {
108
111
  }
109
112
 
110
113
  /**
111
- * 将探测结果格式化为建议式 hint;无文件时返回空字符串。
114
+ * 用显式模板格式化探测结果;无文件或无正文模板时返回空字符串。
112
115
  * scope 限定只报告某一来源:all(默认)/ global($DSH_HOME/AGENTS.md)/ project(cwd→项目根链)。
113
116
  */
114
- export function buildInstructionHintText({ root, projectFiles = [], userGlobalFiles = [], userGlobalHome } = {}, scope = 'all') {
117
+ export function buildInstructionHintText({ root, projectFiles = [], userGlobalFiles = [], userGlobalHome } = {}, scope = 'all', templates = {}) {
115
118
  const sections = []
116
119
  if (scope !== 'global' && projectFiles.length > 0) {
117
- sections.push(`Reference documents exist: ${projectFiles.join(', ')} (project root: ${root}).`)
120
+ const text = interpolateStatic(templateOf(templates, 'projectTemplate'), { FILES: projectFiles.join(', '), ROOT: root ?? '' })
121
+ if (text.trim().length > 0) sections.push(text)
118
122
  }
119
123
  if (scope !== 'project' && userGlobalFiles.length > 0) {
120
124
  const paths = typeof userGlobalHome === 'string' && userGlobalHome.length > 0
121
125
  ? userGlobalFiles.map((name) => joinPath(userGlobalHome, name))
122
126
  : userGlobalFiles
123
- sections.push(`A user reference document exists: ${paths.join(', ')}.`)
127
+ const text = interpolateStatic(templateOf(templates, 'globalTemplate'), { FILES: paths.join(', '), ROOT: root ?? '' })
128
+ if (text.trim().length > 0) sections.push(text)
124
129
  }
125
130
  if (sections.length === 0) return ''
126
- sections.push(REFERENCE_HINT_SUFFIX)
131
+ const suffix = templateOf(templates, 'suffixTemplate')
132
+ if (suffix.trim().length > 0) sections.push(suffix)
127
133
  return sections.join(' ')
128
134
  }
129
135
 
@@ -137,13 +143,13 @@ function hintScope(value) {
137
143
  * prompt-config placeholder fill=instruction-hint 的 resolver。
138
144
  * params.text(自定义提示)优先;params.file(文件卡绑定的单个指令文件)次之——
139
145
  * 运行时读该文件正文并加 `Instructions from:` 头注入;最后按 params.scope 探测来源
140
- * (all / global / project)只发「文件存在」提示。
146
+ * (all / global / project)按显式 projectTemplate/globalTemplate 生成提示。
147
+ * 未配置提示模板时返回 null;引擎不提供默认引导文案。
141
148
  * 文件缺失、不可读或空内容时返回 null,不注入任何消息。
142
149
  */
143
150
  export function createInstructionHintResolver(config = {}) {
144
- const customText = typeof config?.params?.text === 'string' && config.params.text.trim().length > 0
145
- ? config.params.text.trim()
146
- : ''
151
+ const customText = typeof config?.params?.text === 'string' ? config.params.text.trim()
152
+ : Array.isArray(config.texts) ? config.texts.join('\n\n').trim() : ''
147
153
  const scope = hintScope(config?.params?.scope)
148
154
  const file = typeof config?.params?.file === 'string' && config.params.file.trim().length > 0
149
155
  ? config.params.file.trim()
@@ -171,11 +177,12 @@ export function createInstructionHintResolver(config = {}) {
171
177
  ? { id, text: `Instructions from: ${fileLabel}\n\n${content}`, source: { kind: 'instruction-file', form: 'instructions' } }
172
178
  : null
173
179
  }
180
+ if (templateOf(config.params, 'projectTemplate').trim().length === 0 && templateOf(config.params, 'globalTemplate').trim().length === 0) return null
174
181
  const fs = ctx.get('fs')
175
182
  if (fs === undefined) return null
176
183
  const cwd = session.header?.cwd ?? process.cwd()
177
184
  const found = await collectInstructionFiles(fs, cwd, agent.signal)
178
- const text = buildInstructionHintText(found, scope)
185
+ const text = buildInstructionHintText(found, scope, config.params)
179
186
  return text.length > 0
180
187
  ? { id, text, source: { kind: 'instruction-hint', form: 'hint' } }
181
188
  : null
@@ -199,36 +206,41 @@ export function extractInstructionPaths(message) {
199
206
  }
200
207
 
201
208
  /** 构造一次性非命令式 hint 消息;替换已有消息时保留其 id。 */
202
- export function buildInstructionHint(original, paths, sourceName = 'instruction-hint') {
209
+ export function buildInstructionHint(original, paths, sourceName = 'instruction-hint', templates = {}) {
210
+ const template = templateOf(templates, 'messageTemplate')
211
+ if (template.trim().length === 0) return original
212
+ const text = interpolateStatic(template, { FILES: paths.join(', '), SUFFIX: templateOf(templates, 'suffixTemplate') })
213
+ if (text.trim().length === 0) return original
203
214
  return {
204
215
  id: typeof original?.id === 'string' && original.id !== '' ? original.id : `instruction-hint-${randomUUID()}`,
205
216
  role: 'user',
206
217
  content: [{
207
218
  type: 'text',
208
- text: '<system-reminder>\n'
209
- + `Reference documents exist: ${paths.join(', ')}. ${REFERENCE_HINT_SUFFIX}`
210
- + '\n</system-reminder>',
219
+ text,
211
220
  }],
212
221
  source: { kind: 'instruction-hint', form: 'hint', plugin: sourceName },
213
222
  }
214
223
  }
215
224
 
216
225
  /** 将 agent-instructions 全文替换为一次性 hint,后续全文消息丢弃。 */
217
- export function instructionHintMessages(messages, state, sourceName = 'instruction-hint') {
226
+ export function instructionHintMessages(messages, state, sourceName = 'instruction-hint', templates = {}) {
227
+ if (templateOf(templates, 'messageTemplate').trim().length === 0) return messages
218
228
  const kept = []
219
229
  for (const message of messages) {
220
230
  if (message?.source?.kind !== 'agent-instructions') {
221
231
  kept.push(message)
222
232
  continue
223
233
  }
224
- if (state.instructionHinted) continue
225
234
  const paths = extractInstructionPaths(message)
226
235
  if (paths.length === 0) {
227
- kept.push(message)
236
+ if (!state.instructionHinted) kept.push(message)
228
237
  continue
229
238
  }
239
+ const hint = buildInstructionHint(message, paths, sourceName, templates)
240
+ if (hint === message) { kept.push(message); continue }
241
+ if (state.instructionHinted) continue
230
242
  state.instructionHinted = true
231
- kept.push(buildInstructionHint(message, paths, sourceName))
243
+ kept.push(hint)
232
244
  }
233
245
  return kept
234
246
  }
@@ -236,7 +248,7 @@ export function instructionHintMessages(messages, state, sourceName = 'instructi
236
248
  // ── plugin 形态 ─────────────────────────────────────────────────────────────
237
249
 
238
250
  /** Every config key this plugin accepts — anything else is a typo. */
239
- const ALLOWED_KEYS = new Set(['enabled', 'promoteOn', 'includeSubagents'])
251
+ const ALLOWED_KEYS = new Set(['enabled', 'promoteOn', 'includeSubagents', 'projectTemplate', 'globalTemplate', 'suffixTemplate', 'messageTemplate'])
240
252
 
241
253
  /** The visible surface, not the append-only log, owns hint lifetime. */
242
254
  function hasVisibleInstructionHint(session) {
@@ -258,7 +270,7 @@ export function apply(ctx, config) {
258
270
  const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
259
271
  // 开关语义:未声明 = 关闭(组合源为本模块显式写 enabled: true)。
260
272
  const enabled = booleanOption(name, source.enabled, 'enabled', false)
261
- if (!enabled) return
273
+ if (!enabled || templateOf(source, 'messageTemplate').trim().length === 0) return
262
274
 
263
275
  const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
264
276
  const warnOnce = createWarnOnce(ctx, name)
@@ -272,10 +284,14 @@ export function apply(ctx, config) {
272
284
  if (!promotion.status(agent).promoted) return decision
273
285
  if (!Array.isArray(decision.messages)) return decision
274
286
  if (agent?.session === undefined) return decision
287
+ const coordinator = getService(ctx, 'promptToolPreStep')
288
+ const messages = typeof coordinator?.filterInstructions === 'function'
289
+ ? coordinator.filterInstructions(agent, decision.messages)
290
+ : decision.messages
275
291
  // 1 换 1 的转换不能按长度判断(长度相同仍可能已转换),
276
292
  // instructionHintMessages 本身保留非目标消息,直接采用结果。
277
293
  const hintState = { instructionHinted: hasVisibleInstructionHint(agent.session) }
278
- return { ...decision, messages: instructionHintMessages(decision.messages, hintState, name) }
294
+ return { ...decision, messages: instructionHintMessages(messages, hintState, name, source) }
279
295
  } catch (error) {
280
296
  // 转换失败不阻断会话:保留原消息。
281
297
  warnOnce(`${name}: instruction hint conversion failed, keeping messages: ${String((error && error.message) || error)}`)