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.
Files changed (169) hide show
  1. package/README.md +185 -46
  2. package/engine/THIRD_PARTY_LICENSES +25 -0
  3. package/engine/actions.mjs +931 -0
  4. package/engine/anchor-match.mjs +6 -3
  5. package/engine/character-tools.mjs +11 -0
  6. package/engine/classify-task.mjs +18 -0
  7. package/engine/compaction-epoch.mjs +41 -20
  8. package/engine/compositions/library/agent-instructions.yml +9 -0
  9. package/engine/compositions/library/command-goal.yml +2 -6
  10. package/engine/compositions/library/compaction.yml +2 -32
  11. package/engine/compositions/library/delegation-ptc.yml +58 -0
  12. package/engine/compositions/library/delegation.yml +5 -38
  13. package/engine/compositions/library/persistent-shell.yml +4 -13
  14. package/engine/compositions/library/planning.yml +8 -23
  15. package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
  16. package/engine/compositions/library/skill-filesystem.yml +2 -8
  17. package/engine/compositions/library/tool-ask-user.yml +2 -3
  18. package/engine/compositions/library/tool-bash.yml +4 -12
  19. package/engine/compositions/library/tool-cordis.yml +7 -0
  20. package/engine/compositions/library/tool-fs-search.yml +2 -11
  21. package/engine/compositions/library/tool-fs.yml +2 -5
  22. package/engine/compositions/library/tool-goal.yml +2 -6
  23. package/engine/compositions/library/tool-jobs.yml +2 -18
  24. package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
  25. package/engine/compositions/library/tool-plugin-manager.yml +8 -0
  26. package/engine/compositions/library/tool-present.yml +7 -0
  27. package/engine/compositions/library/tool-presentation.yml +9 -0
  28. package/engine/compositions/library/tool-pwsh.yml +2 -6
  29. package/engine/compositions/library/tool-skill.yml +7 -0
  30. package/engine/compositions/library/tool-todo.yml +2 -4
  31. package/engine/compositions/library/tool-web.yml +2 -1
  32. package/engine/compositions/source/local/character-tools.yml +5 -0
  33. package/engine/compositions/source/local/declared-triggers.yml +12 -0
  34. package/engine/compositions/source/local/filesystem-editor.yml +30 -0
  35. package/engine/compositions/source/local/instruction-hint.yml +15 -0
  36. package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
  37. package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
  38. package/engine/compositions/source/local/run-code-env.yml +4 -7
  39. package/engine/compositions/source/local/session-var-tools.yml +5 -0
  40. package/engine/compositions/source/local/skill-search.yml +3 -9
  41. package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
  42. package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
  43. package/engine/compositions/source/local/tool-config-engine.yml +8 -0
  44. package/engine/compositions/source/local/tool-git-bash.yml +15 -0
  45. package/engine/compositions/source/local/world-book-tools.yml +5 -0
  46. package/engine/condition.mjs +83 -0
  47. package/engine/declared-triggers.mjs +93 -0
  48. package/engine/executor.mjs +364 -185
  49. package/engine/fields.mjs +219 -0
  50. package/engine/fillers.mjs +12 -141
  51. package/engine/host-package.mjs +51 -0
  52. package/engine/instruction-hint.mjs +285 -0
  53. package/engine/interpolate.mjs +157 -29
  54. package/engine/layers.mjs +495 -75
  55. package/engine/predicates.mjs +753 -0
  56. package/engine/prompt-config-engine.mjs +63 -8
  57. package/engine/run-code-env.mjs +48 -31
  58. package/engine/schema.mjs +444 -103
  59. package/engine/sdk-strip.mjs +365 -0
  60. package/engine/session-var-tools.mjs +11 -0
  61. package/engine/session-vars.mjs +8 -6
  62. package/engine/shared.mjs +137 -0
  63. package/engine/skill-search.mjs +34 -21
  64. package/engine/st-macros.d.mts +22 -0
  65. package/engine/st-macros.mjs +208 -0
  66. package/engine/st-render.mjs +105 -0
  67. package/engine/st-world-book.d.mts +10 -0
  68. package/engine/st-world-book.mjs +259 -0
  69. package/engine/strategies.mjs +31 -20
  70. package/engine/subagent-tool-policy-core.mjs +400 -0
  71. package/engine/subagent-tool-policy.mjs +327 -0
  72. package/engine/tool-config-engine.mjs +207 -145
  73. package/engine/tool-definition.mjs +143 -0
  74. package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
  75. package/engine/trigger-editor-meta.mjs +48 -0
  76. package/engine/trigger-spec.mjs +257 -0
  77. package/engine/trigger.mjs +304 -0
  78. package/engine/world-book-tools.mjs +11 -0
  79. package/lib/client.js +16495 -5456
  80. package/lib/client.js.map +1 -1
  81. package/lib/index.d.mts +628 -382
  82. package/lib/index.mjs +5391 -2942
  83. package/lib/paths-CY5psBqE.d.mts +1233 -0
  84. package/lib/preset-package-BnJqrU4R.mjs +16792 -0
  85. package/lib/preset-transfer.d.mts +2 -0
  86. package/lib/preset-transfer.mjs +2 -0
  87. package/package.json +91 -49
  88. package/preset/pt-cordis/preset.yml +43 -0
  89. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
  90. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
  91. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
  92. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
  93. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
  94. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
  95. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
  96. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
  97. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
  98. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
  99. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
  100. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
  101. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
  102. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
  103. package/preset/pt-custom/preset.yml +15 -0
  104. package/preset/pt-minimal/preset.yml +27 -0
  105. package/preset/pt-ptc/preset.yml +45 -0
  106. package/preset/pt-standard/preset.yml +43 -0
  107. package/templates/14-first-turn-anchor.yml +4 -4
  108. package/templates/15-guide-auto.yml +3 -3
  109. package/templates/16-custom-fallback.yml +1 -0
  110. package/templates/18-placeholder-env-facts.yml +3 -3
  111. package/templates/20-system-section.yml +3 -2
  112. package/templates/65-turn-stop.yml +18 -0
  113. package/templates/66-subagent-start.yml +14 -0
  114. package/templates/75-subagent-end.yml +15 -0
  115. package/templates/tools/10-shell-command.yml +1 -1
  116. package/templates/tools/40-delegate-character.yml +1 -1
  117. package/templates/tools/50-delegate-worldbook.yml +1 -1
  118. package/templates/tools/60-delegate-session-var.yml +1 -1
  119. package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
  120. package/engine/anchor-turn.mjs +0 -68
  121. package/engine/code-presentation.mjs +0 -103
  122. package/engine/compositions/library/anchor-turn.yml +0 -10
  123. package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
  124. package/engine/compositions/library/code-presentation.yml +0 -8
  125. package/engine/compositions/library/context-gate.yml +0 -39
  126. package/engine/compositions/library/cot-drip.yml +0 -11
  127. package/engine/compositions/library/custom-bash.yml +0 -14
  128. package/engine/compositions/library/deliberation-gate.yml +0 -11
  129. package/engine/compositions/library/official-agent-instructions.yml +0 -17
  130. package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
  131. package/engine/compositions/library/official-tool-bash.yml +0 -6
  132. package/engine/compositions/library/official-tool-cordis.yml +0 -13
  133. package/engine/compositions/library/official-tool-presentation.yml +0 -7
  134. package/engine/compositions/library/official-tool-skill.yml +0 -14
  135. package/engine/compositions/library/persona.yml +0 -13
  136. package/engine/compositions/library/prompt-config-engine.yml +0 -16
  137. package/engine/compositions/library/run-code-env.yml +0 -15
  138. package/engine/compositions/library/skill-search.yml +0 -14
  139. package/engine/compositions/library/str-replace-editor.yml +0 -10
  140. package/engine/compositions/library/tool-bootstrap.yml +0 -14
  141. package/engine/compositions/library/tool-config-engine.yml +0 -9
  142. package/engine/compositions/library/tool-filter.yml +0 -8
  143. package/engine/compositions/source/local/code-presentation.yml +0 -8
  144. package/engine/compositions/source/local/context-gate.yml +0 -36
  145. package/engine/compositions/source/local/custom-bash.yml +0 -11
  146. package/engine/compositions/source/local/str-replace-editor.yml +0 -10
  147. package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
  148. package/engine/compositions/source/local/tool-filter.yml +0 -8
  149. package/engine/context-gate.mjs +0 -305
  150. package/engine/cot-drip.mjs +0 -137
  151. package/engine/deliberation-gate.mjs +0 -127
  152. package/engine/tool-bootstrap.mjs +0 -476
  153. package/engine/tool-filter.mjs +0 -80
  154. package/lib/preset-core.d.mts +0 -10
  155. package/lib/preset-core.mjs +0 -77
  156. package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
  157. package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
  158. package/preset/anchored/preset.yml +0 -296
  159. package/preset/creative/preset.yml +0 -67
  160. package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
  161. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
  162. package/preset/custom/preset.yml +0 -12
  163. package/preset/liangshen/preset.yml +0 -75
  164. package/preset/minimal/preset.yml +0 -45
  165. package/preset/ptc/preset.yml +0 -58
  166. package/preset/standard/preset.yml +0 -57
  167. package/skills/manifest.json +0 -7
  168. package/skills/sandboxmod/SKILL.md +0 -49
  169. package/skills/web ui/SKILL.md +0 -42
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * strategies — 引擎内容策略绑定(config.resolve)。
3
- * 内置策略: static / placeholder / instruction-hint / first-turn-anchor / guide-auto / custom-fallback。
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 { createInstructionHintResolver, createPlaceholderResolver } from './fillers.mjs'
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
- // 自定义文本统一读 text(与 guide-auto 同契约);firstTurnText 旧键兼容。
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
- const firstTurnWord = typeof config.params?.firstTurnWord === 'string' && config.params.firstTurnWord.length > 0
86
- ? config.params.firstTurnWord
87
- : 'we'
82
+ // 确认词无内置默认(默认值归模板/预设):空 = 不做词确认,只走未命中兜底注入。
83
+ const firstTurnWord = typeof config.params?.firstTurnWord === 'string' ? config.params.firstTurnWord : ''
88
84
  // 确认词集合:writePreset 派生的 anchorWords 优先(锚句信号词多词 prefix,任一命中即
89
- // 确认——deep 档 Let…/自定义锚句首词都覆盖);旧产物无 anchorWords 时回退 firstTurnWord 单词。
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
- const anchorScanned = new Map()
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.get(session.id)
111
+ const cached = anchorScanned.peek(session)
101
112
  if (cached !== undefined) return cached
102
- const first = session.events.find((event) => event.type === 'assistant/message')
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.id, confirmed)
118
+ anchorScanned.set(session, confirmed)
108
119
  return confirmed
109
120
  }
110
121
 
111
122
  const assistantRounds = (agent) =>
112
- agent.session.events.filter((event) => event.type === 'assistant/message').length
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
+ }