dsh-plugin-prompt-tool 0.7.2 → 0.8.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 +185 -46
- package/engine/THIRD_PARTY_LICENSES +25 -0
- package/engine/actions.mjs +931 -0
- package/engine/anchor-match.mjs +6 -3
- package/engine/character-tools.mjs +11 -0
- package/engine/classify-task.mjs +18 -0
- package/engine/compaction-epoch.mjs +41 -20
- package/engine/compositions/library/agent-instructions.yml +9 -0
- package/engine/compositions/library/command-goal.yml +2 -6
- package/engine/compositions/library/compaction.yml +2 -32
- package/engine/compositions/library/delegation-ptc.yml +58 -0
- package/engine/compositions/library/delegation.yml +5 -38
- package/engine/compositions/library/persistent-shell.yml +4 -13
- package/engine/compositions/library/planning.yml +8 -23
- package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/skill-filesystem.yml +2 -8
- package/engine/compositions/library/tool-ask-user.yml +2 -3
- package/engine/compositions/library/tool-bash.yml +4 -12
- package/engine/compositions/library/tool-cordis.yml +7 -0
- package/engine/compositions/library/tool-fs-search.yml +2 -11
- package/engine/compositions/library/tool-fs.yml +2 -5
- package/engine/compositions/library/tool-goal.yml +2 -6
- package/engine/compositions/library/tool-jobs.yml +2 -18
- package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
- package/engine/compositions/library/tool-plugin-manager.yml +8 -0
- package/engine/compositions/library/tool-present.yml +7 -0
- package/engine/compositions/library/tool-presentation.yml +9 -0
- package/engine/compositions/library/tool-pwsh.yml +2 -6
- package/engine/compositions/library/tool-skill.yml +7 -0
- package/engine/compositions/library/tool-todo.yml +2 -4
- package/engine/compositions/library/tool-web.yml +2 -1
- package/engine/compositions/source/local/character-tools.yml +5 -0
- package/engine/compositions/source/local/declared-triggers.yml +12 -0
- package/engine/compositions/source/local/filesystem-editor.yml +30 -0
- package/engine/compositions/source/local/instruction-hint.yml +15 -0
- package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
- package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
- package/engine/compositions/source/local/run-code-env.yml +4 -7
- package/engine/compositions/source/local/session-var-tools.yml +5 -0
- package/engine/compositions/source/local/skill-search.yml +3 -9
- package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
- package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
- package/engine/compositions/source/local/tool-config-engine.yml +8 -0
- package/engine/compositions/source/local/tool-git-bash.yml +15 -0
- package/engine/compositions/source/local/world-book-tools.yml +5 -0
- package/engine/condition.mjs +83 -0
- package/engine/declared-triggers.mjs +93 -0
- package/engine/executor.mjs +364 -185
- package/engine/fields.mjs +219 -0
- package/engine/fillers.mjs +12 -141
- package/engine/host-package.mjs +51 -0
- package/engine/instruction-hint.mjs +285 -0
- package/engine/interpolate.mjs +157 -29
- package/engine/layers.mjs +495 -75
- package/engine/predicates.mjs +753 -0
- package/engine/prompt-config-engine.mjs +63 -8
- package/engine/run-code-env.mjs +48 -31
- package/engine/schema.mjs +444 -103
- package/engine/sdk-strip.mjs +365 -0
- package/engine/session-var-tools.mjs +11 -0
- package/engine/session-vars.mjs +8 -6
- package/engine/shared.mjs +137 -0
- package/engine/skill-search.mjs +34 -21
- package/engine/st-macros.d.mts +22 -0
- package/engine/st-macros.mjs +208 -0
- package/engine/st-render.mjs +105 -0
- package/engine/st-world-book.d.mts +10 -0
- package/engine/st-world-book.mjs +259 -0
- package/engine/strategies.mjs +31 -20
- package/engine/subagent-tool-policy-core.mjs +400 -0
- package/engine/subagent-tool-policy.mjs +327 -0
- package/engine/tool-config-engine.mjs +207 -145
- package/engine/tool-definition.mjs +143 -0
- package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
- package/engine/trigger-editor-meta.mjs +48 -0
- package/engine/trigger-spec.mjs +257 -0
- package/engine/trigger.mjs +304 -0
- package/engine/world-book-tools.mjs +11 -0
- package/lib/client.js +16495 -5456
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +628 -382
- package/lib/index.mjs +5391 -2942
- package/lib/paths-CY5psBqE.d.mts +1233 -0
- package/lib/preset-package-BnJqrU4R.mjs +16792 -0
- package/lib/preset-transfer.d.mts +2 -0
- package/lib/preset-transfer.mjs +2 -0
- package/package.json +91 -49
- package/preset/pt-cordis/preset.yml +43 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
- package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
- package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
- package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
- package/preset/pt-custom/preset.yml +15 -0
- package/preset/pt-minimal/preset.yml +27 -0
- package/preset/pt-ptc/preset.yml +45 -0
- package/preset/pt-standard/preset.yml +43 -0
- package/templates/14-first-turn-anchor.yml +4 -4
- package/templates/15-guide-auto.yml +3 -3
- package/templates/16-custom-fallback.yml +1 -0
- package/templates/18-placeholder-env-facts.yml +3 -3
- package/templates/20-system-section.yml +3 -2
- package/templates/65-turn-stop.yml +18 -0
- package/templates/66-subagent-start.yml +14 -0
- package/templates/75-subagent-end.yml +15 -0
- package/templates/tools/10-shell-command.yml +1 -1
- package/templates/tools/40-delegate-character.yml +1 -1
- package/templates/tools/50-delegate-worldbook.yml +1 -1
- package/templates/tools/60-delegate-session-var.yml +1 -1
- package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
- package/engine/anchor-turn.mjs +0 -68
- package/engine/code-presentation.mjs +0 -103
- package/engine/compositions/library/anchor-turn.yml +0 -10
- package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
- package/engine/compositions/library/code-presentation.yml +0 -8
- package/engine/compositions/library/context-gate.yml +0 -39
- package/engine/compositions/library/cot-drip.yml +0 -11
- package/engine/compositions/library/custom-bash.yml +0 -14
- package/engine/compositions/library/deliberation-gate.yml +0 -11
- package/engine/compositions/library/official-agent-instructions.yml +0 -17
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
- package/engine/compositions/library/official-tool-bash.yml +0 -6
- package/engine/compositions/library/official-tool-cordis.yml +0 -13
- package/engine/compositions/library/official-tool-presentation.yml +0 -7
- package/engine/compositions/library/official-tool-skill.yml +0 -14
- package/engine/compositions/library/persona.yml +0 -13
- package/engine/compositions/library/prompt-config-engine.yml +0 -16
- package/engine/compositions/library/run-code-env.yml +0 -15
- package/engine/compositions/library/skill-search.yml +0 -14
- package/engine/compositions/library/str-replace-editor.yml +0 -10
- package/engine/compositions/library/tool-bootstrap.yml +0 -14
- package/engine/compositions/library/tool-config-engine.yml +0 -9
- package/engine/compositions/library/tool-filter.yml +0 -8
- package/engine/compositions/source/local/code-presentation.yml +0 -8
- package/engine/compositions/source/local/context-gate.yml +0 -36
- package/engine/compositions/source/local/custom-bash.yml +0 -11
- package/engine/compositions/source/local/str-replace-editor.yml +0 -10
- package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
- package/engine/compositions/source/local/tool-filter.yml +0 -8
- package/engine/context-gate.mjs +0 -305
- package/engine/cot-drip.mjs +0 -137
- package/engine/deliberation-gate.mjs +0 -127
- package/engine/tool-bootstrap.mjs +0 -476
- package/engine/tool-filter.mjs +0 -80
- package/lib/preset-core.d.mts +0 -10
- package/lib/preset-core.mjs +0 -77
- package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
- package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
- package/preset/anchored/preset.yml +0 -296
- package/preset/creative/preset.yml +0 -67
- package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
- package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
- package/preset/custom/preset.yml +0 -12
- package/preset/liangshen/preset.yml +0 -75
- package/preset/minimal/preset.yml +0 -45
- package/preset/ptc/preset.yml +0 -58
- package/preset/standard/preset.yml +0 -57
- package/skills/manifest.json +0 -7
- package/skills/sandboxmod/SKILL.md +0 -49
- package/skills/web ui/SKILL.md +0 -42
package/engine/strategies.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* strategies — 引擎内容策略绑定(config.resolve)。
|
|
3
|
-
* 内置策略: static / placeholder /
|
|
3
|
+
* 内置策略: static / placeholder / first-turn-anchor / guide-auto / custom-fallback / world-book。
|
|
4
4
|
* 策略参数全部来自 config.params(由 preset.yml 单一配置源下发),引擎只负责组装。
|
|
5
5
|
* 仍支持 strategyDir 懒加载自定义模板策略。
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { extractText } from './shared.mjs'
|
|
8
|
+
import { extractText, sessionEvents, sessionState } from './shared.mjs'
|
|
9
9
|
import { MATCH_LOGIC, createAnchorMatcher } from './anchor-match.mjs'
|
|
10
10
|
import { createTaskClassifier } from './classify-task.mjs'
|
|
11
|
-
import {
|
|
11
|
+
import { createPlaceholderResolver } from './fillers.mjs'
|
|
12
12
|
|
|
13
13
|
const name = 'prompt-config-engine'
|
|
14
14
|
|
|
@@ -18,10 +18,7 @@ const name = 'prompt-config-engine'
|
|
|
18
18
|
*/
|
|
19
19
|
function createFirstTurnAnchorResolver(config) {
|
|
20
20
|
const useCustom = config.params?.useCustom === true
|
|
21
|
-
|
|
22
|
-
const customText = typeof config.params?.text === 'string'
|
|
23
|
-
? config.params.text
|
|
24
|
-
: (typeof config.params?.firstTurnText === 'string' ? config.params.firstTurnText : '')
|
|
21
|
+
const customText = typeof config.params?.text === 'string' ? config.params.text : ''
|
|
25
22
|
const buildPattern = typeof config.params?.buildPattern === 'string' ? config.params.buildPattern : ''
|
|
26
23
|
const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
|
|
27
24
|
const firstTurnBuild = typeof config.params?.firstTurnBuild === 'string' ? config.params.firstTurnBuild : ''
|
|
@@ -82,34 +79,48 @@ function createCustomFallbackResolver(config) {
|
|
|
82
79
|
const promptText = config.texts.length > 0
|
|
83
80
|
? config.texts.join('\n\n')
|
|
84
81
|
: (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
: 'we'
|
|
82
|
+
// 确认词无内置默认(默认值归模板/预设):空 = 不做词确认,只走未命中兜底注入。
|
|
83
|
+
const firstTurnWord = typeof config.params?.firstTurnWord === 'string' ? config.params.firstTurnWord : ''
|
|
88
84
|
// 确认词集合:writePreset 派生的 anchorWords 优先(锚句信号词多词 prefix,任一命中即
|
|
89
|
-
// 确认——deep 档 Let
|
|
85
|
+
// 确认——deep 档 Let…/自定义锚句首词都覆盖);无 anchorWords 时用显式确认词。
|
|
90
86
|
const anchorWords = Array.isArray(config.params?.anchorWords) && config.params.anchorWords.length > 0
|
|
91
87
|
? config.params.anchorWords.map(String).filter((word) => word.length > 0)
|
|
92
|
-
: [firstTurnWord]
|
|
88
|
+
: (firstTurnWord.length > 0 ? [firstTurnWord] : [])
|
|
93
89
|
// 锚定匹配经 anchor-match 引擎(prefix 模式:首轮 reasoning 开头命中任一确认词)。
|
|
94
90
|
const anchor = createAnchorMatcher({ keys: anchorWords, mode: 'prefix' })
|
|
95
91
|
|
|
96
|
-
|
|
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)
|
|
97
108
|
|
|
98
109
|
const isAnchorConfirmed = (agent) => {
|
|
99
110
|
const session = agent.session
|
|
100
|
-
const cached = anchorScanned.
|
|
111
|
+
const cached = anchorScanned.peek(session)
|
|
101
112
|
if (cached !== undefined) return cached
|
|
102
|
-
const first = session.
|
|
113
|
+
const first = sessionEvents(session).find((event) => event.type === 'assistant/message')
|
|
103
114
|
if (first === undefined) return false
|
|
104
115
|
const content = first.data?.message?.content ?? []
|
|
105
116
|
const reasoning = content.find((block) => block.type === 'reasoning')
|
|
106
117
|
const confirmed = reasoning !== undefined && anchor.scan(String(reasoning.text ?? '')).active
|
|
107
|
-
anchorScanned.set(session
|
|
118
|
+
anchorScanned.set(session, confirmed)
|
|
108
119
|
return confirmed
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
const assistantRounds = (agent) =>
|
|
112
|
-
agent.session.
|
|
123
|
+
sessionEvents(agent.session).filter((event) => event.type === 'assistant/message').length
|
|
113
124
|
|
|
114
125
|
return ({ agent }) => {
|
|
115
126
|
if (promptText === undefined) return null
|
|
@@ -123,8 +134,8 @@ function createCustomFallbackResolver(config) {
|
|
|
123
134
|
plugin: config.id,
|
|
124
135
|
form: 'notice',
|
|
125
136
|
summary: confirmed
|
|
126
|
-
? `prompt-tool 提示词(「${firstTurnWord}」锚定确认后注入)`
|
|
127
|
-
: `prompt-tool 提示词(「${firstTurnWord}」未确认,兜底注入)
|
|
137
|
+
? (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」锚定确认后注入)` : 'prompt-tool 提示词(无确认词,锚定就绪后注入)')
|
|
138
|
+
: (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」未确认,兜底注入)` : 'prompt-tool 提示词(无确认词,兜底注入)'),
|
|
128
139
|
},
|
|
129
140
|
}
|
|
130
141
|
}
|
|
@@ -136,6 +147,7 @@ function createCustomFallbackResolver(config) {
|
|
|
136
147
|
* (any=任一命中 / all=副键全中 / not=副键全不中)。匹配经 anchor-match 引擎。
|
|
137
148
|
*/
|
|
138
149
|
function createWorldBookResolver(config) {
|
|
150
|
+
if (config.params?.stWorldBook) return ({ stWorldBookSelected }) => stWorldBookSelected === true ? { text: config.texts.join('\n\n') } : null
|
|
139
151
|
const constant = config.params?.constant === true
|
|
140
152
|
// selectiveLogic:ST world_info_logic 0=AND_ANY 1=NOT_ALL 2=NOT_ANY 3=AND_ALL。
|
|
141
153
|
const rawLogic = config.params?.selectiveLogic
|
|
@@ -181,7 +193,6 @@ function createWorldBookResolver(config) {
|
|
|
181
193
|
export function bindResolver(config, strategyDir) {
|
|
182
194
|
switch (config.strategy) {
|
|
183
195
|
case 'placeholder': return createPlaceholderResolver(config)
|
|
184
|
-
case 'instruction-hint': return createInstructionHintResolver(config)
|
|
185
196
|
case 'first-turn-anchor': return createFirstTurnAnchorResolver(config)
|
|
186
197
|
case 'guide-auto': return createGuideAutoResolver(config)
|
|
187
198
|
case 'custom-fallback':
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* subagent-tool-policy-core — 子代理实例级工具策略纯模块(单一 seam)。
|
|
3
|
+
*
|
|
4
|
+
* Host bridge、运行时 shadow 工具与测试都只经过本模块的四个接口:
|
|
5
|
+
* validateSubagentToolPolicy — 策略 schema/唯一性/引用/子集校验(返回错误列表);
|
|
6
|
+
* compileSubagentToolPolicy — 校验后编译(正则编译、稳定排序、Set 归一);
|
|
7
|
+
* resolveSubagentToolPolicy — 实例选择与有效工具集解析(selector 优先级、
|
|
8
|
+
* expansion/deny 合并、ceiling 硬上限);
|
|
9
|
+
* buildSubagentToolParameters — 模型可见的扩展参数 Schema 片段。
|
|
10
|
+
*
|
|
11
|
+
* 不变量:
|
|
12
|
+
* - ceiling.deny 永远优先,任何 selector / additional_tools 都不能恢复;
|
|
13
|
+
* - 有效工具集 = (profile.allow ∪ 已验证扩权) ∩ ceiling.allow ∩ presetAvailable
|
|
14
|
+
* − profile.deny − restrict_tools − ceiling.deny;
|
|
15
|
+
* - selector 优先级 tool_profile > character_id > task_type > 自动分类 > default;
|
|
16
|
+
* - 纯模块:不 import dsh-tools、不写文件,可在 .engine 与 host 两侧加载。
|
|
17
|
+
*/
|
|
18
|
+
import { createOrderedTaskClassifier } from './classify-task.mjs'
|
|
19
|
+
|
|
20
|
+
/** 解析字符串列表(数组或逗号/空格分隔字符串),非字符串项报错。 */
|
|
21
|
+
function stringList(value, path, errors, { allowEmpty = true } = {}) {
|
|
22
|
+
if (value === undefined || value === null) return []
|
|
23
|
+
const raw = Array.isArray(value) ? value : typeof value === 'string' ? value.split(/[,\s]+/) : undefined
|
|
24
|
+
if (raw === undefined) {
|
|
25
|
+
errors.push(`${path} must be an array of strings`)
|
|
26
|
+
return []
|
|
27
|
+
}
|
|
28
|
+
const out = []
|
|
29
|
+
for (const item of raw) {
|
|
30
|
+
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
31
|
+
errors.push(`${path} must contain only non-empty strings`)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
if (!out.includes(item.trim())) out.push(item.trim())
|
|
35
|
+
}
|
|
36
|
+
return out
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 布尔可选字段。 */
|
|
40
|
+
function booleanOption(value, fallback) {
|
|
41
|
+
return typeof value === 'boolean' ? value : fallback
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 校验策略;返回错误列表(空 = 合法)。 */
|
|
45
|
+
export function validateSubagentToolPolicy(raw) {
|
|
46
|
+
const errors = []
|
|
47
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
48
|
+
return ['subagentToolPolicy must be an object']
|
|
49
|
+
}
|
|
50
|
+
const defaultProfile = raw.defaultProfile
|
|
51
|
+
if (typeof defaultProfile !== 'string' || defaultProfile.length === 0) {
|
|
52
|
+
errors.push('subagentToolPolicy.defaultProfile is required')
|
|
53
|
+
}
|
|
54
|
+
const ceiling = raw.ceiling
|
|
55
|
+
if (ceiling === null || typeof ceiling !== 'object' || Array.isArray(ceiling)) {
|
|
56
|
+
errors.push('subagentToolPolicy.ceiling is required')
|
|
57
|
+
}
|
|
58
|
+
const ceilingAllow = ceiling !== null && typeof ceiling === 'object' && !Array.isArray(ceiling)
|
|
59
|
+
? stringList(ceiling.allow, 'ceiling.allow', errors)
|
|
60
|
+
: []
|
|
61
|
+
if (ceilingAllow.length === 0) errors.push('subagentToolPolicy.ceiling.allow must be a non-empty list')
|
|
62
|
+
const ceilingDeny = ceiling !== null && typeof ceiling === 'object' && !Array.isArray(ceiling)
|
|
63
|
+
? stringList(ceiling.deny, 'ceiling.deny', errors)
|
|
64
|
+
: []
|
|
65
|
+
|
|
66
|
+
const profileIds = new Set()
|
|
67
|
+
const profiles = Array.isArray(raw.profiles) ? raw.profiles : []
|
|
68
|
+
if (profiles.length === 0) errors.push('subagentToolPolicy.profiles must declare at least one profile')
|
|
69
|
+
for (const [index, profile] of profiles.entries()) {
|
|
70
|
+
const path = `profiles[${index}]`
|
|
71
|
+
if (profile === null || typeof profile !== 'object' || Array.isArray(profile)) {
|
|
72
|
+
errors.push(`${path} must be an object`)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
if (typeof profile.id !== 'string' || profile.id.length === 0) {
|
|
76
|
+
errors.push(`${path}.id is required`)
|
|
77
|
+
} else {
|
|
78
|
+
if (profileIds.has(profile.id)) errors.push(`${path}.id ${JSON.stringify(profile.id)} is duplicated`)
|
|
79
|
+
profileIds.add(profile.id)
|
|
80
|
+
}
|
|
81
|
+
if (typeof profile.name !== 'string' || profile.name.length === 0) {
|
|
82
|
+
errors.push(`${path}.name is required`)
|
|
83
|
+
}
|
|
84
|
+
const allow = stringList(profile.allow, `${path}.allow`, errors)
|
|
85
|
+
const deny = stringList(profile.deny, `${path}.deny`, errors)
|
|
86
|
+
for (const tool of allow) {
|
|
87
|
+
if (!ceilingAllow.includes(tool)) errors.push(`${path}.allow tool ${JSON.stringify(tool)} exceeds ceiling.allow`)
|
|
88
|
+
}
|
|
89
|
+
for (const tool of deny) {
|
|
90
|
+
if (!ceilingAllow.includes(tool)) errors.push(`${path}.deny tool ${JSON.stringify(tool)} is not in ceiling.allow`)
|
|
91
|
+
}
|
|
92
|
+
if (typeof profile.modelSelectable !== 'undefined' && typeof profile.modelSelectable !== 'boolean') {
|
|
93
|
+
errors.push(`${path}.modelSelectable must be a boolean`)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const characterIds = new Set()
|
|
98
|
+
const characterBindings = Array.isArray(raw.characterBindings) ? raw.characterBindings : []
|
|
99
|
+
for (const [index, binding] of characterBindings.entries()) {
|
|
100
|
+
const path = `characterBindings[${index}]`
|
|
101
|
+
if (binding === null || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
102
|
+
errors.push(`${path} must be an object`)
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
if (typeof binding.characterId !== 'string' || binding.characterId.length === 0) {
|
|
106
|
+
errors.push(`${path}.characterId is required`)
|
|
107
|
+
} else {
|
|
108
|
+
if (characterIds.has(binding.characterId)) errors.push(`${path}.characterId ${JSON.stringify(binding.characterId)} is duplicated`)
|
|
109
|
+
characterIds.add(binding.characterId)
|
|
110
|
+
}
|
|
111
|
+
if (typeof binding.profile !== 'string' || !profileIds.has(binding.profile)) {
|
|
112
|
+
errors.push(`${path}.profile ${JSON.stringify(binding.profile)} must reference an existing profile`)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const taskIds = new Set()
|
|
117
|
+
const taskRules = Array.isArray(raw.taskRules) ? raw.taskRules : []
|
|
118
|
+
for (const [index, rule] of taskRules.entries()) {
|
|
119
|
+
const path = `taskRules[${index}]`
|
|
120
|
+
if (rule === null || typeof rule !== 'object' || Array.isArray(rule)) {
|
|
121
|
+
errors.push(`${path} must be an object`)
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
if (typeof rule.id !== 'string' || rule.id.length === 0) {
|
|
125
|
+
errors.push(`${path}.id is required`)
|
|
126
|
+
} else {
|
|
127
|
+
if (taskIds.has(rule.id)) errors.push(`${path}.id ${JSON.stringify(rule.id)} is duplicated`)
|
|
128
|
+
taskIds.add(rule.id)
|
|
129
|
+
}
|
|
130
|
+
if (typeof rule.pattern !== 'string' || rule.pattern.length === 0) {
|
|
131
|
+
errors.push(`${path}.pattern is required`)
|
|
132
|
+
} else {
|
|
133
|
+
try {
|
|
134
|
+
// 不用全局 g 标志;编译期校验非法正则,非法拒绝整次保存。
|
|
135
|
+
new RegExp(rule.pattern)
|
|
136
|
+
} catch (error) {
|
|
137
|
+
errors.push(`${path}.pattern is not a valid regular expression: ${String(error.message ?? error)}`)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (typeof rule.order !== 'number' || !Number.isSafeInteger(rule.order)) {
|
|
141
|
+
errors.push(`${path}.order must be an integer`)
|
|
142
|
+
}
|
|
143
|
+
if (typeof rule.profile !== 'string' || !profileIds.has(rule.profile)) {
|
|
144
|
+
errors.push(`${path}.profile ${JSON.stringify(rule.profile)} must reference an existing profile`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const expansion = raw.modelExpansion
|
|
149
|
+
if (expansion !== undefined && expansion !== null) {
|
|
150
|
+
const expansionPath = 'modelExpansion'
|
|
151
|
+
if (typeof expansion !== 'object' || Array.isArray(expansion)) {
|
|
152
|
+
errors.push(`${expansionPath} must be an object`)
|
|
153
|
+
} else {
|
|
154
|
+
const expansionAllow = stringList(expansion.allow, `${expansionPath}.allow`, errors)
|
|
155
|
+
for (const tool of expansionAllow) {
|
|
156
|
+
if (!ceilingAllow.includes(tool)) errors.push(`${expansionPath}.allow tool ${JSON.stringify(tool)} exceeds ceiling.allow`)
|
|
157
|
+
}
|
|
158
|
+
if (expansion.maxAdditionalTools !== undefined
|
|
159
|
+
&& (!Number.isSafeInteger(expansion.maxAdditionalTools) || expansion.maxAdditionalTools < 0)) {
|
|
160
|
+
errors.push(`${expansionPath}.maxAdditionalTools must be a non-negative safe integer`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (defaultProfile !== undefined && typeof defaultProfile === 'string' && defaultProfile.length > 0 && !profileIds.has(defaultProfile)) {
|
|
165
|
+
errors.push(`subagentToolPolicy.defaultProfile ${JSON.stringify(defaultProfile)} must reference an existing profile`)
|
|
166
|
+
}
|
|
167
|
+
return errors
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** 编译策略:校验通过后归一为运行态结构(Set / 编译正则 / 稳定排序 / 分类器)。 */
|
|
171
|
+
export function compileSubagentToolPolicy(raw) {
|
|
172
|
+
const errors = validateSubagentToolPolicy(raw)
|
|
173
|
+
if (errors.length > 0) {
|
|
174
|
+
throw new Error(`invalid subagentToolPolicy: ${errors.join('; ')}`)
|
|
175
|
+
}
|
|
176
|
+
const ceiling = raw.ceiling
|
|
177
|
+
const ceilingAllow = stringList(ceiling.allow, 'ceiling.allow', [])
|
|
178
|
+
const ceilingDeny = stringList(ceiling.deny, 'ceiling.deny', [])
|
|
179
|
+
const profiles = (Array.isArray(raw.profiles) ? raw.profiles : []).map((profile) => ({
|
|
180
|
+
id: profile.id,
|
|
181
|
+
name: profile.name,
|
|
182
|
+
allow: stringList(profile.allow, 'profile.allow', []),
|
|
183
|
+
deny: stringList(profile.deny, 'profile.deny', []),
|
|
184
|
+
modelSelectable: booleanOption(profile.modelSelectable, false),
|
|
185
|
+
}))
|
|
186
|
+
const profileMap = new Map(profiles.map((profile) => [profile.id, profile]))
|
|
187
|
+
const characterBindings = (Array.isArray(raw.characterBindings) ? raw.characterBindings : []).map((binding) => ({
|
|
188
|
+
characterId: binding.characterId,
|
|
189
|
+
profile: binding.profile,
|
|
190
|
+
modelSelectable: booleanOption(binding.modelSelectable, false),
|
|
191
|
+
}))
|
|
192
|
+
const taskRules = (Array.isArray(raw.taskRules) ? raw.taskRules : [])
|
|
193
|
+
.map((rule) => ({
|
|
194
|
+
id: rule.id,
|
|
195
|
+
name: rule.name ?? rule.id,
|
|
196
|
+
pattern: rule.pattern,
|
|
197
|
+
re: new RegExp(rule.pattern),
|
|
198
|
+
profile: rule.profile,
|
|
199
|
+
order: rule.order,
|
|
200
|
+
modelSelectable: booleanOption(rule.modelSelectable, false),
|
|
201
|
+
}))
|
|
202
|
+
.sort((a, b) => (a.order - b.order) || 0)
|
|
203
|
+
const expansionRaw = raw.modelExpansion
|
|
204
|
+
const expansion = {
|
|
205
|
+
enabled: booleanOption(expansionRaw?.enabled, false),
|
|
206
|
+
allow: stringList(expansionRaw?.allow, 'modelExpansion.allow', []),
|
|
207
|
+
maxAdditionalTools: Number.isSafeInteger(expansionRaw?.maxAdditionalTools) ? expansionRaw.maxAdditionalTools : 0,
|
|
208
|
+
requireApproval: booleanOption(expansionRaw?.requireApproval, true),
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
defaultProfileId: raw.defaultProfile,
|
|
212
|
+
ceiling: { allow: ceilingAllow, deny: ceilingDeny },
|
|
213
|
+
profiles,
|
|
214
|
+
profileMap,
|
|
215
|
+
characterBindings,
|
|
216
|
+
taskRules,
|
|
217
|
+
expansion,
|
|
218
|
+
modelSelectableProfiles: profiles.filter((profile) => profile.modelSelectable).map((profile) => profile.id),
|
|
219
|
+
modelSelectableCharacterIds: characterBindings.filter((binding) => binding.modelSelectable).map((binding) => binding.characterId),
|
|
220
|
+
modelSelectableTaskTypes: taskRules.filter((rule) => rule.modelSelectable).map((rule) => rule.id),
|
|
221
|
+
classify: createOrderedTaskClassifier(taskRules),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 解析一次实例的工具集。request 字段:
|
|
227
|
+
* tool_profile / character_id / task_type / additional_tools / restrict_tools /
|
|
228
|
+
* description / prompt(自动分类输入 = description + "\n" + prompt)。
|
|
229
|
+
* availableTools:当前 preset scope 可解析的继承工具名集合。
|
|
230
|
+
*/
|
|
231
|
+
export function resolveSubagentToolPolicy(compiled, request, availableTools) {
|
|
232
|
+
const requestRecord = request ?? {}
|
|
233
|
+
const presetAvailable = new Set(Array.isArray(availableTools) ? availableTools : [])
|
|
234
|
+
const adopted = []
|
|
235
|
+
const ignored = []
|
|
236
|
+
|
|
237
|
+
const explicitProfile = typeof requestRecord.tool_profile === 'string' && requestRecord.tool_profile.length > 0
|
|
238
|
+
? requestRecord.tool_profile
|
|
239
|
+
: undefined
|
|
240
|
+
const explicitCharacter = typeof requestRecord.character_id === 'string' && requestRecord.character_id.length > 0
|
|
241
|
+
? requestRecord.character_id
|
|
242
|
+
: undefined
|
|
243
|
+
const explicitTask = typeof requestRecord.task_type === 'string' && requestRecord.task_type.length > 0
|
|
244
|
+
? requestRecord.task_type
|
|
245
|
+
: undefined
|
|
246
|
+
|
|
247
|
+
// 选择基础 profile:tool_profile > character_id 绑定 > task_type > 自动分类 > default。
|
|
248
|
+
let selectedProfileId = compiled.defaultProfileId
|
|
249
|
+
let selectedCharacterId
|
|
250
|
+
let selectedTaskType
|
|
251
|
+
let selected = false
|
|
252
|
+
if (explicitProfile !== undefined) {
|
|
253
|
+
if (compiled.modelSelectableProfiles.includes(explicitProfile)) {
|
|
254
|
+
selectedProfileId = explicitProfile
|
|
255
|
+
adopted.push('tool_profile')
|
|
256
|
+
selected = true
|
|
257
|
+
} else {
|
|
258
|
+
throw new Error(`tool_profile ${JSON.stringify(explicitProfile)} is not model-selectable`)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (selected && explicitCharacter !== undefined) ignored.push('character_id')
|
|
262
|
+
if (selected && explicitTask !== undefined) ignored.push('task_type')
|
|
263
|
+
if (!selected && explicitCharacter !== undefined) {
|
|
264
|
+
const binding = compiled.characterBindings.find((item) => item.characterId === explicitCharacter && item.modelSelectable)
|
|
265
|
+
if (binding !== undefined) {
|
|
266
|
+
selectedProfileId = binding.profile
|
|
267
|
+
selectedCharacterId = explicitCharacter
|
|
268
|
+
adopted.push('character_id')
|
|
269
|
+
selected = true
|
|
270
|
+
if (explicitTask !== undefined) ignored.push('task_type')
|
|
271
|
+
} else {
|
|
272
|
+
throw new Error(`character_id ${JSON.stringify(explicitCharacter)} is not model-selectable`)
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (!selected && explicitTask !== undefined) {
|
|
276
|
+
const rule = compiled.taskRules.find((item) => item.id === explicitTask && item.modelSelectable)
|
|
277
|
+
if (rule !== undefined) {
|
|
278
|
+
selectedProfileId = rule.profile
|
|
279
|
+
selectedTaskType = explicitTask
|
|
280
|
+
adopted.push('task_type')
|
|
281
|
+
selected = true
|
|
282
|
+
} else {
|
|
283
|
+
throw new Error(`task_type ${JSON.stringify(explicitTask)} is not model-selectable`)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (!selected) {
|
|
287
|
+
// 未显式选择时按 description + prompt 自动分类。
|
|
288
|
+
const autoText = [requestRecord.description, requestRecord.prompt].filter((item) => typeof item === 'string').join('\n')
|
|
289
|
+
const autoRuleId = compiled.classify(autoText)
|
|
290
|
+
const autoRule = autoRuleId !== undefined ? compiled.taskRules.find((rule) => rule.id === autoRuleId) : undefined
|
|
291
|
+
if (autoRule !== undefined) {
|
|
292
|
+
selectedProfileId = autoRule.profile
|
|
293
|
+
selectedTaskType = autoRuleId
|
|
294
|
+
adopted.push('auto-classify')
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const profile = compiled.profileMap.get(selectedProfileId) ?? compiled.profileMap.get(compiled.defaultProfileId)
|
|
298
|
+
const baseAllow = profile?.allow ?? []
|
|
299
|
+
|
|
300
|
+
// 扩权校验:requestedAdd ∩ modelExpansion.allow ∩ ceiling.allow ∩ presetAvailable。
|
|
301
|
+
if (requestRecord.additional_tools !== undefined && !Array.isArray(requestRecord.additional_tools)) {
|
|
302
|
+
throw new Error('additional_tools must be an array of tool names')
|
|
303
|
+
}
|
|
304
|
+
const requestedAdd = Array.isArray(requestRecord.additional_tools) ? requestRecord.additional_tools : []
|
|
305
|
+
if (requestedAdd.some((tool) => typeof tool !== 'string' || tool.length === 0)) {
|
|
306
|
+
throw new Error('additional_tools must contain only non-empty tool names')
|
|
307
|
+
}
|
|
308
|
+
const uniqueRequestedAdd = [...new Set(requestedAdd)]
|
|
309
|
+
if (requestedAdd.length !== uniqueRequestedAdd.length) {
|
|
310
|
+
throw new Error('additional_tools must not contain duplicates')
|
|
311
|
+
}
|
|
312
|
+
if (uniqueRequestedAdd.length > 0 && (!compiled.expansion.enabled || compiled.expansion.maxAdditionalTools === 0)) {
|
|
313
|
+
throw new Error('additional_tools is disabled by subagentToolPolicy')
|
|
314
|
+
}
|
|
315
|
+
if (uniqueRequestedAdd.length > compiled.expansion.maxAdditionalTools) {
|
|
316
|
+
throw new Error(`additional_tools exceeds maxAdditionalTools (${compiled.expansion.maxAdditionalTools})`)
|
|
317
|
+
}
|
|
318
|
+
const validatedAdd = []
|
|
319
|
+
if (uniqueRequestedAdd.length > 0) {
|
|
320
|
+
for (const tool of uniqueRequestedAdd) {
|
|
321
|
+
if (!compiled.expansion.allow.includes(tool)) throw new Error(`additional tool ${JSON.stringify(tool)} is not expansion-authorized`)
|
|
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`)
|
|
324
|
+
validatedAdd.push(tool)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
if (requestRecord.restrict_tools !== undefined && !Array.isArray(requestRecord.restrict_tools)) {
|
|
328
|
+
throw new Error('restrict_tools must be an array of tool names')
|
|
329
|
+
}
|
|
330
|
+
const requestedDeny = Array.isArray(requestRecord.restrict_tools) ? requestRecord.restrict_tools : []
|
|
331
|
+
if (requestedDeny.some((tool) => typeof tool !== 'string' || tool.length === 0)) {
|
|
332
|
+
throw new Error('restrict_tools must contain only non-empty tool names')
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 有效工具集 = (baseAllow ∪ validatedAdd) ∩ ceiling.allow ∩ presetAvailable − deny 三件套。
|
|
336
|
+
const effective = new Set([...baseAllow, ...validatedAdd])
|
|
337
|
+
const ceilingAllowSet = new Set(compiled.ceiling.allow)
|
|
338
|
+
const denySet = new Set([...(profile?.deny ?? []), ...requestedDeny, ...compiled.ceiling.deny])
|
|
339
|
+
for (const tool of [...effective]) {
|
|
340
|
+
if (!ceilingAllowSet.has(tool)) effective.delete(tool)
|
|
341
|
+
if (!presetAvailable.has(tool)) effective.delete(tool)
|
|
342
|
+
if (denySet.has(tool)) effective.delete(tool)
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
adopted,
|
|
346
|
+
ignored,
|
|
347
|
+
profileId: selectedProfileId,
|
|
348
|
+
characterId: selectedCharacterId,
|
|
349
|
+
taskType: selectedTaskType,
|
|
350
|
+
additionalTools: validatedAdd,
|
|
351
|
+
effectiveTools: [...effective].sort(),
|
|
352
|
+
requiresApproval: validatedAdd.length > 0 && compiled.expansion.requireApproval,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* 模型可见的扩展参数 Schema 片段(JSON Schema property 映射;未开启能力不出现)。
|
|
358
|
+
* 运行时 shadow 工具把它与官方 subagent 参数合并成完整 parameters。
|
|
359
|
+
*/
|
|
360
|
+
export function buildSubagentToolParameters(compiled) {
|
|
361
|
+
const properties = {}
|
|
362
|
+
if (compiled.modelSelectableProfiles.length > 0) {
|
|
363
|
+
properties.tool_profile = {
|
|
364
|
+
type: 'string',
|
|
365
|
+
enum: compiled.modelSelectableProfiles,
|
|
366
|
+
description: '子代理工具档(模型可选择的 profile id)',
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (compiled.modelSelectableCharacterIds.length > 0) {
|
|
370
|
+
properties.character_id = {
|
|
371
|
+
type: 'string',
|
|
372
|
+
enum: compiled.modelSelectableCharacterIds,
|
|
373
|
+
description: '角色卡绑定(模型可选择的 character id)',
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (compiled.modelSelectableTaskTypes.length > 0) {
|
|
377
|
+
properties.task_type = {
|
|
378
|
+
type: 'string',
|
|
379
|
+
enum: compiled.modelSelectableTaskTypes,
|
|
380
|
+
description: '任务类型(模型可选择的 task rule id)',
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (compiled.expansion.enabled && compiled.expansion.maxAdditionalTools > 0) {
|
|
384
|
+
const expansionEnum = compiled.expansion.allow.filter((tool) => compiled.ceiling.allow.includes(tool))
|
|
385
|
+
if (expansionEnum.length > 0) {
|
|
386
|
+
properties.additional_tools = {
|
|
387
|
+
type: 'array',
|
|
388
|
+
items: { type: 'string', enum: expansionEnum },
|
|
389
|
+
maxItems: compiled.expansion.maxAdditionalTools,
|
|
390
|
+
description: '模型请求的附加工具(受用户配置上限约束)',
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
properties.restrict_tools = {
|
|
395
|
+
type: 'array',
|
|
396
|
+
items: { type: 'string' },
|
|
397
|
+
description: '模型可进一步收紧本实例的工具名列表',
|
|
398
|
+
}
|
|
399
|
+
return properties
|
|
400
|
+
}
|