dsh-plugin-prompt-tool 1.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +38 -29
  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/compaction-epoch.mjs +31 -13
  20. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  21. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  22. package/engine/compositions/source/local/rule-engine.yml +6 -0
  23. package/engine/condition.mjs +16 -3
  24. package/engine/conditions/anchor.mjs +32 -0
  25. package/engine/conditions/availability.mjs +47 -0
  26. package/engine/conditions/composite.mjs +83 -0
  27. package/engine/conditions/count.mjs +140 -0
  28. package/engine/conditions/evaluation.mjs +41 -0
  29. package/engine/conditions/examples.mjs +11 -0
  30. package/engine/conditions/index.mjs +85 -0
  31. package/engine/conditions/names.mjs +21 -0
  32. package/engine/conditions/phase.mjs +54 -0
  33. package/engine/conditions/preset.mjs +94 -0
  34. package/engine/conditions/scope.mjs +18 -0
  35. package/engine/conditions/session.mjs +22 -0
  36. package/engine/conditions/source.mjs +63 -0
  37. package/engine/conditions/subject.mjs +121 -0
  38. package/engine/conditions/text.mjs +44 -0
  39. package/engine/conditions/values.mjs +32 -0
  40. package/engine/dev-tool-search.mjs +213 -0
  41. package/engine/executor.mjs +55 -6
  42. package/engine/fillers.mjs +23 -16
  43. package/engine/instruction-hint.mjs +34 -22
  44. package/engine/layers.mjs +135 -49
  45. package/engine/order.mjs +4 -1
  46. package/engine/predicates.mjs +2 -753
  47. package/engine/rule-engine.mjs +40 -0
  48. package/engine/rule-runtime.mjs +110 -0
  49. package/engine/rule-spec.mjs +187 -0
  50. package/engine/schema.mjs +16 -5
  51. package/engine/shared.mjs +1 -1
  52. package/engine/strategies.mjs +2 -235
  53. package/engine/subagent-tool-policy-core.mjs +7 -7
  54. package/engine/subagent-tool-policy.mjs +20 -10
  55. package/engine/tool-config-engine.mjs +53 -32
  56. package/engine/trigger-spec.mjs +49 -133
  57. package/lib/client.js +11117 -10860
  58. package/lib/client.js.map +1 -1
  59. package/lib/index.d.mts +82 -139
  60. package/lib/index.mjs +826 -1087
  61. package/lib/migrate-rules.d.mts +1 -0
  62. package/lib/migrate-rules.mjs +39 -0
  63. package/lib/module-package-CHPFweqK.mjs +2 -0
  64. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  65. package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
  66. package/lib/preset-transfer.d.mts +2 -2
  67. package/lib/preset-transfer.mjs +3 -2
  68. package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
  69. package/modules/ponytail/module.yml +381 -147
  70. package/modules/skill-surface/module.yml +74 -0
  71. package/modules/tool-surface/module.yml +67 -0
  72. package/package.json +5 -1
  73. package/templates/10-pre-step.yml +26 -22
  74. package/templates/14-first-turn-anchor.yml +25 -21
  75. package/templates/15-guide-auto.yml +34 -19
  76. package/templates/16-custom-fallback.yml +27 -15
  77. package/templates/18-placeholder-env-facts.yml +23 -14
  78. package/templates/20-system-section.yml +16 -10
  79. package/templates/30-runtime-context.yml +13 -7
  80. package/templates/40-agent-request.yml +11 -8
  81. package/templates/50-llm-stream.yml +14 -6
  82. package/templates/60-tool-pipeline.yml +15 -9
  83. package/templates/65-turn-stop.yml +19 -12
  84. package/templates/66-subagent-start.yml +12 -7
  85. package/templates/70-subagent-maintenance.yml +35 -29
  86. package/templates/75-subagent-end.yml +10 -5
  87. package/templates/80-tool-surface.yml +66 -0
  88. package/templates/policies/legacy-defaults.yml +34 -0
  89. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  90. package/engine/prompt-config-engine.mjs +0 -104
  91. package/engine/trigger-editor-meta.mjs +0 -48
  92. package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
  93. package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
  94. package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
  95. package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
@@ -1,235 +1,2 @@
1
- /**
2
- * strategies — 引擎内容策略绑定(config.resolve)。
3
- * 内置策略: static / placeholder / first-turn-anchor / guide-auto / custom-fallback / world-book。
4
- * 策略参数全部来自 config.params(由 preset.yml 单一配置源下发),引擎只负责组装。
5
- * 仍支持 strategyDir 懒加载自定义模板策略。
6
- */
7
-
8
- import { extractText, sessionEvents, sessionState } from './shared.mjs'
9
- import { MATCH_LOGIC, createAnchorMatcher } from './anchor-match.mjs'
10
- import { createTaskClassifier } from './classify-task.mjs'
11
- import { createPlaceholderResolver } from './fillers.mjs'
12
-
13
- const name = 'prompt-config-engine'
14
-
15
- /**
16
- * first-turn-anchor:首条真实用户消息后的一次性任务锚点。
17
- * 正则与锚句文本全部来自 config.params(由 preset.yml 单一配置源下发)。
18
- */
19
- function createFirstTurnAnchorResolver(config) {
20
- const useCustom = config.params?.useCustom === true
21
- const customText = typeof config.params?.text === 'string' ? config.params.text : ''
22
- const buildPattern = typeof config.params?.buildPattern === 'string' ? config.params.buildPattern : ''
23
- const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
24
- const firstTurnBuild = typeof config.params?.firstTurnBuild === 'string' ? config.params.firstTurnBuild : ''
25
- const firstTurnInspect = typeof config.params?.firstTurnInspect === 'string' ? config.params.firstTurnInspect : ''
26
- const firstTurnDeep = typeof config.params?.firstTurnDeep === 'string' ? config.params.firstTurnDeep : ''
27
- const classifier = createTaskClassifier({ buildPattern, complexPattern })
28
- return ({ messages }) => {
29
- if (useCustom) {
30
- const text = customText.trim()
31
- return text.length > 0 ? { text } : null
32
- }
33
- if (!classifier.ready || (firstTurnBuild.length === 0 && firstTurnInspect.length === 0 && firstTurnDeep.length === 0)) {
34
- return null
35
- }
36
- const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
37
- if (userIndex < 0) return null
38
- const taskText = extractText(messages[userIndex])
39
- if (taskText.length === 0) return null
40
- const kind = classifier.classify(taskText)
41
- const anchor = kind === 'complex' ? firstTurnDeep : kind === 'build' ? firstTurnBuild : firstTurnInspect
42
- return anchor.length > 0 ? { text: anchor } : null
43
- }
44
- }
45
-
46
- /**
47
- * guide-auto:晋升后每轮用户消息后的弱/深度引导。
48
- * 正则与引导文本全部来自 config.params(由 preset.yml 单一配置源下发)。
49
- * 复杂任务判定 fallback 复用锚定功能的 complexPattern(guideComplexPattern
50
- * 冗余副本已移除)——锚定与引导是独立功能,仅分类器共用。
51
- */
52
- function createGuideAutoResolver(config) {
53
- const useCustom = config.params?.useCustom === true
54
- const customText = typeof config.params?.text === 'string' ? config.params.text : ''
55
- const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
56
- const guideWeak = typeof config.params?.guideWeak === 'string' ? config.params.guideWeak : ''
57
- const guideDeep = typeof config.params?.guideDeep === 'string' ? config.params.guideDeep : ''
58
- // 复杂判定 fallback 复用锚定分类器(buildPattern 不参与引导判定)。
59
- const classifier = createTaskClassifier({ complexPattern })
60
- return ({ messages }) => {
61
- const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
62
- if (userIndex < 0) return null
63
- const text = extractText(messages[userIndex])
64
- if (text.length === 0) return null
65
- if (useCustom) {
66
- const guide = customText.trim()
67
- return guide.length > 0 ? { text: guide } : null
68
- }
69
- if (guideWeak.length === 0 && guideDeep.length === 0) return null
70
- return { text: (text.length > 120 || classifier.isComplex(text)) ? guideDeep : guideWeak }
71
- }
72
- }
73
-
74
- /**
75
- * custom-fallback:自定义锚定词命中后注入一次,未命中最多两轮兜底。
76
- * 参数全部来自 config.params(由 preset.yml 单一配置源下发)。
77
- */
78
- function createCustomFallbackResolver(config) {
79
- const promptText = config.texts.length > 0
80
- ? config.texts.join('\n\n')
81
- : (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
82
- // 确认词无内置默认(默认值归模板/预设):空 = 不做词确认,只走未命中兜底注入。
83
- const firstTurnWord = typeof config.params?.firstTurnWord === 'string' ? config.params.firstTurnWord : ''
84
- // 确认词集合:writePreset 派生的 anchorWords 优先(锚句信号词多词 prefix,任一命中即
85
- // 确认——deep 档 Let…/自定义锚句首词都覆盖);无 anchorWords 时用显式确认词。
86
- const anchorWords = Array.isArray(config.params?.anchorWords) && config.params.anchorWords.length > 0
87
- ? config.params.anchorWords.map(String).filter((word) => word.length > 0)
88
- : (firstTurnWord.length > 0 ? [firstTurnWord] : [])
89
- // 锚定匹配经 anchor-match 引擎(prefix 模式:首轮 reasoning 开头命中任一确认词)。
90
- const anchor = createAnchorMatcher({ keys: anchorWords, mode: 'prefix' })
91
-
92
- /**
93
- * 锚定确认结果缓存(`sessionState`:统一访问接口,键 / 淘汰 / 复位三项与迁移前逐条相同)。
94
- *
95
- * - 键类型:`session.id`(进程内快路径,真相仍是 durable 事件流)。
96
- * - 淘汰策略:超 `MAX_TRACKED_SESSIONS` 时 `clear()` 全清(`evict` 缺省档)。
97
- * - 复位:**不声明**——本缓存纯派生自事件流(首条 assistant/message 的 reasoning 块),
98
- * 没有复位语义;未声明 `reset` 时 `sessionState` 不订阅任何事件,`ctx` 完全用不到。
99
- *
100
- * `create` 不可达(本缓存只用 `peek`/`set`:未命中必须扫 durable 事件,不能凭空造值)。
101
- * 缓存值是布尔,`false` 是**有效命中**,所以命中判定必须用 `peek(session) !== undefined`。
102
- * 未找到首条 assistant/message 时返回 `false` 且**不写缓存**(决策未定,等下一条事件)。
103
- * 无 `session.id` 的会话不记账(`sessionState` 既有决定,见 `predicates.mjs`):每次重扫
104
- * 该会话自己的事件流得同一结论。旧实现把这类会话缓存到共享的 `undefined` 键上,第二个
105
- * 无 id 会话会读到第一个的缓存——现在以 durable 事实为准。
106
- */
107
- const anchorScanned = sessionState(undefined, () => false)
108
-
109
- const isAnchorConfirmed = (agent) => {
110
- const session = agent.session
111
- const cached = anchorScanned.peek(session)
112
- if (cached !== undefined) return cached
113
- const first = sessionEvents(session).find((event) => event.type === 'assistant/message')
114
- if (first === undefined) return false
115
- const content = first.data?.message?.content ?? []
116
- const reasoning = content.find((block) => block.type === 'reasoning')
117
- const confirmed = reasoning !== undefined && anchor.scan(String(reasoning.text ?? '')).active
118
- anchorScanned.set(session, confirmed)
119
- return confirmed
120
- }
121
-
122
- const assistantRounds = (agent) =>
123
- sessionEvents(agent.session).filter((event) => event.type === 'assistant/message').length
124
-
125
- return ({ agent }) => {
126
- if (promptText === undefined) return null
127
- const session = agent.session
128
- const confirmed = isAnchorConfirmed(agent)
129
- if (!confirmed && assistantRounds(agent) <= 1) return null
130
- return {
131
- text: promptText,
132
- source: {
133
- kind: 'plugin',
134
- plugin: config.id,
135
- form: 'notice',
136
- summary: confirmed
137
- ? (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」锚定确认后注入)` : 'prompt-tool 提示词(无确认词,锚定就绪后注入)')
138
- : (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」未确认,兜底注入)` : 'prompt-tool 提示词(无确认词,兜底注入)'),
139
- },
140
- }
141
- }
142
- }
143
-
144
- /**
145
- * world-book:世界书条目(角色卡 lorebook)。constant=true 恒注入(不扫 keys);
146
- * selective 条目扫描当前消息批文本,keys/secondaryKeys 按 selectiveLogic 组合
147
- * (any=任一命中 / all=副键全中 / not=副键全不中)。匹配经 anchor-match 引擎。
148
- */
149
- function createWorldBookResolver(config) {
150
- if (config.params?.stWorldBook) return ({ stWorldBookSelected }) => stWorldBookSelected === true ? { text: config.texts.join('\n\n') } : null
151
- const constant = config.params?.constant === true
152
- // selectiveLogic:ST world_info_logic 0=AND_ANY 1=NOT_ALL 2=NOT_ANY 3=AND_ALL。
153
- const rawLogic = config.params?.selectiveLogic
154
- const logic = rawLogic === 3
155
- ? MATCH_LOGIC.ALL
156
- : (rawLogic === 1
157
- ? MATCH_LOGIC.NOT
158
- : (rawLogic === 2 ? MATCH_LOGIC.NOT_ANY : MATCH_LOGIC.ANY))
159
- const matcher = createAnchorMatcher({
160
- keys: Array.isArray(config.params?.keys) ? config.params.keys : [],
161
- secondaryKeys: Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : [],
162
- caseSensitive: config.params?.caseSensitive === true,
163
- wholeWords: config.params?.wholeWords === true,
164
- // 三态透传:undefined=ST 语义自动检测(/regex/ 或含特殊字符键按正则);
165
- // 存量配置显式 true/false 仍强制对应行为。
166
- useRegex: config.params?.useRegex,
167
- logic,
168
- })
169
- // ST 语义:constant 或无任何键的条目恒注入(always active)。
170
- const hasAnyKey = (Array.isArray(config.params?.keys) ? config.params.keys : [])
171
- .some((key) => String(key).trim().length > 0)
172
- || (Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : [])
173
- .some((key) => String(key).trim().length > 0)
174
- const promptText = config.texts.length > 0
175
- ? config.texts.join('\n\n')
176
- : (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
177
- return ({ messages }) => {
178
- if (promptText === undefined) return null
179
- if (constant || !hasAnyKey) return { text: promptText }
180
- const haystack = (Array.isArray(messages) ? messages : [])
181
- .map((message) => extractText(message))
182
- .filter((text) => text.length > 0)
183
- .join('\n')
184
- if (haystack.length === 0) return null
185
- return matcher.scan(haystack).active ? { text: promptText } : null
186
- }
187
- }
188
-
189
- /**
190
- * 为归一化后的提示词配置绑定策略 resolve(策略状态随提示词配置对象,不随 apply)。
191
- * strategyDir:外部策略模块目录(相对本文件 URL)。未声明时只允许内置策略。
192
- */
193
- export function bindResolver(config, strategyDir) {
194
- switch (config.strategy) {
195
- case 'placeholder': return createPlaceholderResolver(config)
196
- case 'first-turn-anchor': return createFirstTurnAnchorResolver(config)
197
- case 'guide-auto': return createGuideAutoResolver(config)
198
- case 'custom-fallback':
199
- return createCustomFallbackResolver(config)
200
- case 'world-book':
201
- return createWorldBookResolver(config)
202
- case 'static': {
203
- const texts = config.texts
204
- const patch = config.templatePatch ?? {}
205
- return () => {
206
- if (texts.length > 0) return { ...patch, content: texts.map((item) => ({ type: 'text', text: item })) }
207
- return null
208
- }
209
- }
210
- default: {
211
- // 模板专属策略:懒加载 <strategyDir>/<strategy>.mjs,模块约定导出
212
- // `createResolver(config)` 或默认导出同名工厂。加载失败按单配置失败语义
213
- // 抛给调用方(executor 会跳过该配置并 warnOnce)。
214
- if (typeof strategyDir !== 'string' || strategyDir.length === 0) {
215
- throw new TypeError(`${name}: unknown config strategy ${JSON.stringify(config.strategy)}`)
216
- }
217
- const moduleUrl = new URL(`${config.strategy}.mjs`, strategyDir.endsWith('/') ? strategyDir : `${strategyDir}/`)
218
- let loaded
219
- return async (args) => {
220
- if (loaded === undefined) {
221
- loaded = await import(moduleUrl.href)
222
- }
223
- const make = typeof loaded.createResolver === 'function'
224
- ? loaded.createResolver
225
- : typeof loaded.default === 'function'
226
- ? loaded.default
227
- : undefined
228
- if (typeof make !== 'function') {
229
- throw new TypeError(`${name}: strategy module ${moduleUrl.href} must export createResolver(config)`)
230
- }
231
- return make(config)(args)
232
- }
233
- }
234
- }
235
- }
1
+ /** 历史导入入口;内容生成真实实现归动作目录。 */
2
+ export { bindResolver } from './actions/content.mjs'
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * 不变量:
12
12
  * - ceiling.deny 永远优先,任何 selector / additional_tools 都不能恢复;
13
- * - 有效工具集 = (profile.allow ∪ 已验证扩权) ∩ ceiling.allow ∩ presetAvailable
13
+ * - 有效工具集 = (profile.allow ∪ 已验证扩权) ∩ ceiling.allow ∩ availableToolNames
14
14
  * − profile.deny − restrict_tools − ceiling.deny;
15
15
  * - selector 优先级 tool_profile > character_id > task_type > 自动分类 > default;
16
16
  * - 纯模块:不 import dsh-tools、不写文件,可在 .engine 与 host 两侧加载。
@@ -226,11 +226,11 @@ export function compileSubagentToolPolicy(raw) {
226
226
  * 解析一次实例的工具集。request 字段:
227
227
  * tool_profile / character_id / task_type / additional_tools / restrict_tools /
228
228
  * description / prompt(自动分类输入 = description + "\n" + prompt)。
229
- * availableTools:当前 preset scope 可解析的继承工具名集合。
229
+ * availableTools:当前 assembly scope 可解析的继承工具名集合;公开位置参数保持不变。
230
230
  */
231
231
  export function resolveSubagentToolPolicy(compiled, request, availableTools) {
232
232
  const requestRecord = request ?? {}
233
- const presetAvailable = new Set(Array.isArray(availableTools) ? availableTools : [])
233
+ const availableToolNames = new Set(Array.isArray(availableTools) ? availableTools : [])
234
234
  const adopted = []
235
235
  const ignored = []
236
236
 
@@ -297,7 +297,7 @@ export function resolveSubagentToolPolicy(compiled, request, availableTools) {
297
297
  const profile = compiled.profileMap.get(selectedProfileId) ?? compiled.profileMap.get(compiled.defaultProfileId)
298
298
  const baseAllow = profile?.allow ?? []
299
299
 
300
- // 扩权校验:requestedAdd ∩ modelExpansion.allow ∩ ceiling.allow ∩ presetAvailable。
300
+ // 扩权校验:requestedAdd ∩ modelExpansion.allow ∩ ceiling.allow ∩ availableToolNames。
301
301
  if (requestRecord.additional_tools !== undefined && !Array.isArray(requestRecord.additional_tools)) {
302
302
  throw new Error('additional_tools must be an array of tool names')
303
303
  }
@@ -320,7 +320,7 @@ export function resolveSubagentToolPolicy(compiled, request, availableTools) {
320
320
  for (const tool of uniqueRequestedAdd) {
321
321
  if (!compiled.expansion.allow.includes(tool)) throw new Error(`additional tool ${JSON.stringify(tool)} is not expansion-authorized`)
322
322
  if (!compiled.ceiling.allow.includes(tool)) throw new Error(`additional tool ${JSON.stringify(tool)} exceeds ceiling.allow`)
323
- if (!presetAvailable.has(tool)) throw new Error(`additional tool ${JSON.stringify(tool)} is not available in this preset`)
323
+ if (!availableToolNames.has(tool)) throw new Error(`additional tool ${JSON.stringify(tool)} is not available in this assembly`)
324
324
  validatedAdd.push(tool)
325
325
  }
326
326
  }
@@ -332,13 +332,13 @@ export function resolveSubagentToolPolicy(compiled, request, availableTools) {
332
332
  throw new Error('restrict_tools must contain only non-empty tool names')
333
333
  }
334
334
 
335
- // 有效工具集 = (baseAllow ∪ validatedAdd) ∩ ceiling.allow ∩ presetAvailable − deny 三件套。
335
+ // 有效工具集 = (baseAllow ∪ validatedAdd) ∩ ceiling.allow ∩ availableToolNames − deny 三件套。
336
336
  const effective = new Set([...baseAllow, ...validatedAdd])
337
337
  const ceilingAllowSet = new Set(compiled.ceiling.allow)
338
338
  const denySet = new Set([...(profile?.deny ?? []), ...requestedDeny, ...compiled.ceiling.deny])
339
339
  for (const tool of [...effective]) {
340
340
  if (!ceilingAllowSet.has(tool)) effective.delete(tool)
341
- if (!presetAvailable.has(tool)) effective.delete(tool)
341
+ if (!availableToolNames.has(tool)) effective.delete(tool)
342
342
  if (denySet.has(tool)) effective.delete(tool)
343
343
  }
344
344
  return {
@@ -2,7 +2,7 @@
2
2
  * subagent-tool-policy — agent-scoped subagent/subagent_fork shadow 工具。
3
3
  *
4
4
  * 本模块通过运行中 DSH 入口解析同一份 dsh-tools / dsh-scope,避免生成目录
5
- * 加载第二份 registry 类型。每个 shadow 只安装到当前 preset generation 的
5
+ * 加载第二份 registry 类型。每个 shadow 只安装到当前 assembly generation 的
6
6
  * descendant Agent;实例权限在 SubagentStartRequest 创建窗口冻结。
7
7
  *
8
8
  * ── 能力提供者边界(T4)────────────────────────────────────────────────────
@@ -23,7 +23,7 @@
23
23
  * 安全边界(T4 明令不得触碰,改动本模块时同样不得放宽):
24
24
  * - 扩权审批门 expansionApproval:requested additional_tools 必须先拿到
25
25
  * `allowed-once`,无 approval 通道时 fail loud(不静默放行);
26
- * - fail loud 语义:策略文件存在但内容非法一律抛错;只有 ENOENT(用户关掉能力卡)
26
+ * - fail loud 语义:显式内联或策略文件内容非法一律抛错;只有路径入口的 ENOENT
27
27
  * 才降级为官方委派行为。
28
28
  *
29
29
  * 登记入口:`engineProvider`(数据导出),供边界守卫消费。提供者登记与声明式触发器
@@ -64,19 +64,28 @@ const SHADOW_TOOL_NAMES = { spawn: 'subagent', fork: 'subagent_fork' }
64
64
 
65
65
  /**
66
66
  * 配置契约:白名单由字段声明派生(此前没有白名单,未知键被静默忽略)。
67
- * 五个键**全部**用 passthrough:迁移前它们对非字符串/非对象值都是静默取默认或忽略
67
+ * 五个旧键保留 passthrough:它们对非字符串/非对象值都是静默取默认或忽略
68
68
  *(policyFile / spawnProvider / forkProvider 取默认名,agentOptions 非对象则 undefined,
69
69
  * maxDepth 原样透传、由下游按 `'provider-managed'` 或数字自行判定),
70
- * 换成严格字段类型会引入 B2 未授权的行为变更。归一化结果与迁移前逐字段等价,
71
- * 唯一新增的是「未知键报错」。
70
+ * 内联 policy 单独完整校验;缺字段才启用路径模式,显式非法值不得降级。
72
71
  */
73
- export const configContract = defineConfig({
72
+ const configFields = defineConfig({
74
73
  policyFile: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : '../subagent-tools/policy.yml')),
74
+ policy: passthrough(value => value),
75
75
  spawnProvider: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : 'spawn')),
76
76
  forkProvider: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : 'fork')),
77
77
  maxDepth: passthrough((value) => value),
78
78
  agentOptions: passthrough((value) => value),
79
79
  })
80
+ export const configContract = {
81
+ ...configFields,
82
+ parse(config, plugin) {
83
+ const source = configFields.parse(config, plugin)
84
+ if (Object.hasOwn(config ?? {}, 'policy')) compileSubagentToolPolicy(source.policy)
85
+ else delete source.policy
86
+ return source
87
+ },
88
+ }
80
89
 
81
90
  /**
82
91
  * 能力提供者登记(T4 边界守卫的数据源)。
@@ -98,6 +107,7 @@ function resolvePolicyFile(config) {
98
107
  }
99
108
 
100
109
  function loadCompiledPolicy(config) {
110
+ if (Object.hasOwn(config, 'policy')) return compileSubagentToolPolicy(config.policy)
101
111
  const raw = readFileSync(resolvePolicyFile(config), 'utf8')
102
112
  const parsed = parseYaml(raw, { logLevel: 'silent' })
103
113
  if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
@@ -210,7 +220,7 @@ async function settleForeground(run) {
210
220
  return { kind: 'foreground', runId: run.id, output: result.output }
211
221
  }
212
222
 
213
- function availablePresetTools(tools, compositionScope) {
223
+ function availableAssemblyTools(tools, compositionScope) {
214
224
  return tools.schemas(compositionScope).map((schema) => schema.name)
215
225
  }
216
226
 
@@ -226,7 +236,7 @@ function createShadowTool(ctx, tools, compositionScope, compiled, kind, config)
226
236
  execute: async (args, run) => {
227
237
  validateShadowArgs(args, compiled, kind)
228
238
  if (run.agent === undefined) throw new Error('no active agent for subagent start')
229
- const resolved = resolveSubagentToolPolicy(compiled, args, availablePresetTools(tools, compositionScope))
239
+ const resolved = resolveSubagentToolPolicy(compiled, args, availableAssemblyTools(tools, compositionScope))
230
240
  if (resolved.requiresApproval) await expansionApproval(ctx, run, toolName, resolved)
231
241
  const childProvider = ctx.subagents.getProvider(provider)
232
242
  if (childProvider === undefined) throw new Error(`subagent provider ${provider} is not registered`)
@@ -274,7 +284,7 @@ function createShadowTool(ctx, tools, compositionScope, compiled, kind, config)
274
284
  }
275
285
 
276
286
  export function apply(ctx, config) {
277
- // 校验入口:未知键在挂载期报错;五个键的归一化结果与迁移前逐字段一致(见 configContract)。
287
+ // 内联 policy 在准备期即可校验;旧配置字段仍保留历史归一化结果。
278
288
  const source = configContract.parse(config, name)
279
289
  let compiled
280
290
  try {
@@ -290,7 +300,7 @@ export function apply(ctx, config) {
290
300
  throw new Error(`${name}: cannot load policy: ${message}`)
291
301
  }
292
302
  const compositionScope = scopeOf(ctx)
293
- if (compositionScope === undefined) throw new Error(`${name}: requires a preset scope`)
303
+ if (compositionScope === undefined) throw new Error(`${name}: requires an assembly scope`)
294
304
  const installs = new Map()
295
305
  const installing = new WeakSet()
296
306
  let active = true
@@ -1,12 +1,12 @@
1
1
  /**
2
2
  * tool-config-engine — 自定义工具引擎(配置驱动 → ctx.tools.register)。
3
3
  *
4
- * 工具定义从 configsDir 目录加载(writePreset 渲染 custom-tools/*.yml,
5
- * 源 = preset.yml 顶层 customTools 段),每份一个工具定义:
4
+ * 工具定义优先从 tools 内联读取,缺字段时兼容 configsDir 目录。
5
+ * 源 = module.yml 顶层 customTools 段,每份一个工具定义:
6
6
  * id / name / description / timeoutMs / parameters(标准 JSON Schema
7
- * object,writePreset 已用官方 @deepseek-ai/dsh-tools 的
7
+ * object,装配方已用官方 @deepseek-ai/dsh-tools 的
8
8
  * parameterSchemaSpecToJsonSchema / valueSchemaSpecToJsonSchema 把
9
- * preset.yml 的 dsh-tools DSL 转换)/ output.schema / execute.{kind, ...}
9
+ * module.yml 的 dsh-tools DSL 转换)/ output.schema / execute.{kind, ...}
10
10
  *
11
11
  * 执行器(kind):
12
12
  * shell — 命令执行(execFile 无 shell 解析,env 白名单,cwd=会话工作区)
@@ -18,7 +18,7 @@
18
18
  * ask-user — approval 通道询问用户(结果文本化)
19
19
  *
20
20
  * 安全:行 config.requireApproval = [kind...] 时该执行器先过 approval 门
21
- * (无 approval 服务则拒绝);单条定义非法 warnOnce 跳过(不挂整行)。
21
+ * (无 approval 服务则拒绝);内联定义整批校验,旧路径模式保留逐条告警跳过。
22
22
  * 工具注册经 disposer 契约(keepDisposer),插件卸载自动撤销。
23
23
  *
24
24
  * ── 能力提供者边界(T4)────────────────────────────────────────────────────
@@ -27,7 +27,7 @@
27
27
  * - 会给模型提供可调用能力(注册工具 / 域 / 服务)的 → 能力提供者;
28
28
  * - 干预流程(改提示词、改装配、裁决、追加)的 → 声明式触发器。
29
29
  * 因此本模块**不接入** engine/trigger.mjs:它不订阅装配 waterfall,也没有
30
- * when/do 声明,只在装配期把 preset.yml 的 customTools 段物化出的工具注册进官方
30
+ * when/do 声明,只在装配期把 module.yml 的 customTools 段编译出的工具注册进官方
31
31
  * registry。强行并进触发器会让引擎同时承担「提供能力」与「干预流程」两种职责。
32
32
  *
33
33
  * 样板收敛:
@@ -347,8 +347,8 @@ function sanitizeDescription(text) {
347
347
  return text.replace(/\{\{([^{}]*)\}\}/g, '{$1}')
348
348
  }
349
349
 
350
- /** 单份工具定义文件 → 编译为完整官方 ToolDefinition(不含 id)。
351
- * parameters/output.schema 已是 writePreset 物化的标准 JSON Schema;无参数
350
+ /** 单份工具定义 → 编译为完整官方 ToolDefinition(不含 id)。
351
+ * parameters/output.schema 已是装配方编译的标准 JSON Schema;无参数
352
352
  * 工具补空 object Schema(真实 ctx.tools.schemas() 需要 parameters 存在)。 */
353
353
  function compileTool(ctx, def, requireApproval) {
354
354
  return {
@@ -364,20 +364,20 @@ function compileTool(ctx, def, requireApproval) {
364
364
  }
365
365
  }
366
366
 
367
- /** 工具定义文件加载(*.yml / *.json;与 prompt-config-engine configsDir 同构)。 */
368
- function loadToolFiles(dirUrl, presetRootUrl) {
367
+ /** 兼容独立入口的工具定义文件加载(*.yml / *.json)。 */
368
+ function loadToolFiles(dirUrl, resourceRootUrl) {
369
369
  const resolved = isAbsolute(dirUrl)
370
370
  ? pathToFileURL(dirUrl).href
371
371
  : dirUrl
372
372
  const dir = new URL(resolved.endsWith('/') ? resolved : `${resolved}/`, import.meta.url)
373
373
  const localDir = fileURLToPath(dir)
374
- // 相对 configsDir 只允许解析到预设根内:防组合行声明越界目录;绝对路径(显式配置/
375
- // 测试桩)保持允许。预设根基准显式注入优先(引擎由插件包提供时必需),缺省沿用
374
+ // 相对 configsDir 只允许解析到资源允许根内;绝对路径(显式配置/测试桩)保持允许。
375
+ // 允许根显式注入优先(引擎由插件包提供时必需),缺省沿用
376
376
  // 「引擎上一级目录」这一历史布局推导。
377
377
  if (!isAbsolute(dirUrl)) {
378
- const presetRoot = fileURLToPath(presetRootUrl ?? new URL('../..', import.meta.url)).replace(/[\\\\/]$/, '')
379
- if (localDir !== presetRoot && !localDir.startsWith(presetRoot + sep)) {
380
- throw new Error(`${name}: configsDir ${JSON.stringify(dirUrl)} escapes preset root`)
378
+ const resourceRoot = fileURLToPath(resourceRootUrl ?? new URL('../..', import.meta.url)).replace(/[\\\\/]$/, '')
379
+ if (localDir !== resourceRoot && !localDir.startsWith(resourceRoot + sep)) {
380
+ throw new Error(`${name}: configsDir ${JSON.stringify(dirUrl)} escapes resource root`)
381
381
  }
382
382
  }
383
383
  const files = readdirSync(localDir, { withFileTypes: true })
@@ -396,40 +396,61 @@ function loadToolFiles(dirUrl, presetRootUrl) {
396
396
  * 配置契约:白名单由字段声明派生(此前没有白名单,未知键被静默忽略)。
397
397
  * requireApproval 迁移前对**非数组取 `[]`**、对数组**过滤掉非字符串项**(宽容而非报错),
398
398
  * configsDir 对非字符串/空串取默认值——两者都用 passthrough 保住该语义。
399
- * 本任务只新增「未知键报错」,缺键与错类型的既有行为逐字段不变。
399
+ * tools 是显式内联输入,缺字段与空数组分开处理;旧字段保留原来的宽容语义。
400
400
  */
401
- export const configContract = defineConfig({
401
+ const configFields = defineConfig({
402
402
  configsDir: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : './custom-tools')),
403
+ tools: passthrough(value => value),
403
404
  requireApproval: passthrough((value) => (Array.isArray(value) ? value.filter((kind) => typeof kind === 'string') : [])),
404
- // 预设根基准(绝对 file URL):引擎由插件包提供时由装配期注入,替代引擎位置推导。
405
+ // 资源允许根(绝对 file URL),保留旧名输入;两者都不改变历史路径授权边界。
406
+ resourceRoot: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : undefined)),
405
407
  presetRoot: passthrough((value) => (typeof value === 'string' && value.length > 0 ? value : undefined)),
406
408
  })
409
+ export const configContract = {
410
+ ...configFields,
411
+ parse(config, plugin) {
412
+ const source = configFields.parse(config, plugin)
413
+ if (source.resourceRoot !== undefined && source.presetRoot !== undefined && source.resourceRoot !== source.presetRoot) {
414
+ throw new TypeError(`${plugin}: resourceRoot and presetRoot conflict`)
415
+ }
416
+ source.resourceRoot ??= source.presetRoot
417
+ if (Object.hasOwn(config ?? {}, 'tools')) {
418
+ if (!Array.isArray(source.tools)) throw new TypeError(`${plugin}: tools must be an array of tool definitions`)
419
+ // 在任何注册前完整校验,坏内联不得回落旧目录或留下半份工具。
420
+ for (const definition of source.tools) validateDefinition(definition)
421
+ } else delete source.tools
422
+ return source
423
+ },
424
+ }
407
425
 
408
426
  /**
409
427
  * 能力提供者登记(T4 边界守卫的数据源)。
410
- * `provides.kind === 'dynamic'`:模型可见的工具名由 preset.yml 的 customTools 段在
411
- * 装配期决定(物化为 custom-tools/*.yml),引擎侧无法静态枚举——守卫只断言登记形状、
428
+ * `provides.kind === 'dynamic'`:模型可见的工具名由 module.yml 的 customTools 段在
429
+ * 装配期决定,引擎侧无法静态枚举——守卫只断言登记形状、
412
430
  * 注册通道,以及「提供者不导出触发器声明」这条互斥。
413
431
  */
414
432
  export const engineProvider = {
415
433
  kind: 'provider',
416
434
  moduleId: name,
417
435
  registers: 'tools',
418
- provides: { kind: 'dynamic', source: 'preset.yml#customTools → custom-tools/*.yml' },
436
+ provides: { kind: 'dynamic', source: 'module.yml#customTools → tools' },
419
437
  }
420
438
 
421
- /** 插件入口:扫描 configsDir 注册全部自定义工具。 */
439
+ /** 插件入口:优先注册内联 tools,缺字段时兼容 configsDir。 */
422
440
  export function apply(ctx, config) {
423
- const { configsDir: dirName, requireApproval, presetRoot: rawPresetRoot } = configContract.parse(config, name)
424
- const presetRootUrl = rawPresetRoot === undefined
425
- ? undefined
426
- : new URL(rawPresetRoot.endsWith('/') ? rawPresetRoot : `${rawPresetRoot}/`)
427
- let definitions = []
428
- try {
429
- definitions = loadToolFiles(dirName, presetRootUrl)
430
- } catch (error) {
431
- ctx.logger?.warn(`${name}: cannot load ${dirName}: ${error?.message ?? error}`)
432
- return
441
+ const source = configContract.parse(config, name)
442
+ const { configsDir: dirName, requireApproval, resourceRoot: rawResourceRoot } = source
443
+ let definitions = source.tools
444
+ if (!Object.hasOwn(source, 'tools')) {
445
+ const resourceRootUrl = rawResourceRoot === undefined
446
+ ? undefined
447
+ : new URL(rawResourceRoot.endsWith('/') ? rawResourceRoot : `${rawResourceRoot}/`)
448
+ try {
449
+ definitions = loadToolFiles(dirName, resourceRootUrl)
450
+ } catch (error) {
451
+ ctx.logger?.warn(`${name}: cannot load ${dirName}: ${error?.message ?? error}`)
452
+ return
453
+ }
433
454
  }
434
455
  for (const def of definitions) {
435
456
  try {