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.
- package/README.md +38 -29
- package/engine/actions/append-context.mjs +49 -0
- package/engine/actions/assembly.mjs +150 -0
- package/engine/actions/catalog.mjs +92 -0
- package/engine/actions/content.mjs +205 -0
- package/engine/actions/decision.mjs +54 -0
- package/engine/actions/examples.mjs +11 -0
- package/engine/actions/guard.mjs +80 -0
- package/engine/actions/inbox-prepend.mjs +37 -0
- package/engine/actions/index.mjs +100 -0
- package/engine/actions/inject-text.mjs +22 -0
- package/engine/actions/pre-step-filter.mjs +55 -0
- package/engine/actions/registration.mjs +168 -0
- package/engine/actions/request-params.mjs +32 -0
- package/engine/actions/sdk-strip.mjs +30 -0
- package/engine/actions/shared.mjs +71 -0
- package/engine/actions.mjs +2 -931
- package/engine/branch.mjs +44 -0
- package/engine/compaction-epoch.mjs +31 -13
- package/engine/compositions/source/local/dev-tool-search.yml +11 -0
- package/engine/compositions/source/local/instruction-hint.yml +22 -1
- package/engine/compositions/source/local/rule-engine.yml +6 -0
- package/engine/condition.mjs +16 -3
- package/engine/conditions/anchor.mjs +32 -0
- package/engine/conditions/availability.mjs +47 -0
- package/engine/conditions/composite.mjs +83 -0
- package/engine/conditions/count.mjs +140 -0
- package/engine/conditions/evaluation.mjs +41 -0
- package/engine/conditions/examples.mjs +11 -0
- package/engine/conditions/index.mjs +85 -0
- package/engine/conditions/names.mjs +21 -0
- package/engine/conditions/phase.mjs +54 -0
- package/engine/conditions/preset.mjs +94 -0
- package/engine/conditions/scope.mjs +18 -0
- package/engine/conditions/session.mjs +22 -0
- package/engine/conditions/source.mjs +63 -0
- package/engine/conditions/subject.mjs +121 -0
- package/engine/conditions/text.mjs +44 -0
- package/engine/conditions/values.mjs +32 -0
- package/engine/dev-tool-search.mjs +213 -0
- package/engine/executor.mjs +55 -6
- package/engine/fillers.mjs +23 -16
- package/engine/instruction-hint.mjs +34 -22
- package/engine/layers.mjs +135 -49
- package/engine/order.mjs +4 -1
- package/engine/predicates.mjs +2 -753
- package/engine/rule-engine.mjs +40 -0
- package/engine/rule-runtime.mjs +110 -0
- package/engine/rule-spec.mjs +187 -0
- package/engine/schema.mjs +16 -5
- package/engine/shared.mjs +1 -1
- package/engine/strategies.mjs +2 -235
- package/engine/subagent-tool-policy-core.mjs +7 -7
- package/engine/subagent-tool-policy.mjs +20 -10
- package/engine/tool-config-engine.mjs +53 -32
- package/engine/trigger-spec.mjs +49 -133
- package/lib/client.js +11117 -10860
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +82 -139
- package/lib/index.mjs +826 -1087
- package/lib/migrate-rules.d.mts +1 -0
- package/lib/migrate-rules.mjs +39 -0
- package/lib/module-package-CHPFweqK.mjs +2 -0
- package/lib/module-package-Cnd1_mwx.mjs +2872 -0
- package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
- package/lib/preset-transfer.d.mts +2 -2
- package/lib/preset-transfer.mjs +3 -2
- package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
- package/modules/ponytail/module.yml +381 -147
- package/modules/skill-surface/module.yml +74 -0
- package/modules/tool-surface/module.yml +67 -0
- package/package.json +5 -1
- package/templates/10-pre-step.yml +26 -22
- package/templates/14-first-turn-anchor.yml +25 -21
- package/templates/15-guide-auto.yml +34 -19
- package/templates/16-custom-fallback.yml +27 -15
- package/templates/18-placeholder-env-facts.yml +23 -14
- package/templates/20-system-section.yml +16 -10
- package/templates/30-runtime-context.yml +13 -7
- package/templates/40-agent-request.yml +11 -8
- package/templates/50-llm-stream.yml +14 -6
- package/templates/60-tool-pipeline.yml +15 -9
- package/templates/65-turn-stop.yml +19 -12
- package/templates/66-subagent-start.yml +12 -7
- package/templates/70-subagent-maintenance.yml +35 -29
- package/templates/75-subagent-end.yml +10 -5
- package/templates/80-tool-surface.yml +66 -0
- package/templates/policies/legacy-defaults.yml +34 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
- package/engine/prompt-config-engine.mjs +0 -104
- package/engine/trigger-editor-meta.mjs +0 -48
- package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
- package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
- package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
- package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
package/engine/strategies.mjs
CHANGED
|
@@ -1,235 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
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 ∩
|
|
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:当前
|
|
229
|
+
* availableTools:当前 assembly scope 可解析的继承工具名集合;公开位置参数保持不变。
|
|
230
230
|
*/
|
|
231
231
|
export function resolveSubagentToolPolicy(compiled, request, availableTools) {
|
|
232
232
|
const requestRecord = request ?? {}
|
|
233
|
-
const
|
|
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 ∩
|
|
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 (!
|
|
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 ∩
|
|
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 (!
|
|
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 只安装到当前
|
|
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
|
|
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
|
-
*
|
|
67
|
+
* 五个旧键保留 passthrough:它们对非字符串/非对象值都是静默取默认或忽略
|
|
68
68
|
*(policyFile / spawnProvider / forkProvider 取默认名,agentOptions 非对象则 undefined,
|
|
69
69
|
* maxDepth 原样透传、由下游按 `'provider-managed'` 或数字自行判定),
|
|
70
|
-
*
|
|
71
|
-
* 唯一新增的是「未知键报错」。
|
|
70
|
+
* 内联 policy 单独完整校验;缺字段才启用路径模式,显式非法值不得降级。
|
|
72
71
|
*/
|
|
73
|
-
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
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
|
|
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
|
-
*
|
|
5
|
-
* 源 =
|
|
4
|
+
* 工具定义优先从 tools 内联读取,缺字段时兼容 configsDir 目录。
|
|
5
|
+
* 源 = module.yml 顶层 customTools 段,每份一个工具定义:
|
|
6
6
|
* id / name / description / timeoutMs / parameters(标准 JSON Schema
|
|
7
|
-
* object
|
|
7
|
+
* object,装配方已用官方 @deepseek-ai/dsh-tools 的
|
|
8
8
|
* parameterSchemaSpecToJsonSchema / valueSchemaSpecToJsonSchema 把
|
|
9
|
-
*
|
|
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
|
|
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 声明,只在装配期把
|
|
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
|
-
/**
|
|
351
|
-
* parameters/output.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
|
-
/**
|
|
368
|
-
function loadToolFiles(dirUrl,
|
|
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
|
|
379
|
-
if (localDir !==
|
|
380
|
-
throw new Error(`${name}: configsDir ${JSON.stringify(dirUrl)} escapes
|
|
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
|
-
|
|
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
|
-
//
|
|
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'`:模型可见的工具名由
|
|
411
|
-
*
|
|
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: '
|
|
436
|
+
provides: { kind: 'dynamic', source: 'module.yml#customTools → tools' },
|
|
419
437
|
}
|
|
420
438
|
|
|
421
|
-
/**
|
|
439
|
+
/** 插件入口:优先注册内联 tools,缺字段时兼容 configsDir。 */
|
|
422
440
|
export function apply(ctx, config) {
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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 {
|