dsh-plugin-prompt-tool 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +100 -233
  2. package/engine/actions/append-context.mjs +3 -24
  3. package/engine/actions/assembly.mjs +70 -94
  4. package/engine/actions/catalog.mjs +65 -28
  5. package/engine/actions/content.mjs +8 -152
  6. package/engine/actions/examples.mjs +8 -4
  7. package/engine/actions/inbox-prepend.mjs +4 -5
  8. package/engine/actions/index.mjs +17 -9
  9. package/engine/actions/inject-text.mjs +24 -4
  10. package/engine/actions/pre-step-filter.mjs +3 -4
  11. package/engine/actions/registration.mjs +20 -28
  12. package/engine/actions/request-params.mjs +21 -1
  13. package/engine/actions/shared.mjs +22 -13
  14. package/engine/actions/stream-output.mjs +26 -0
  15. package/engine/actions/tool-gate.mjs +31 -0
  16. package/engine/actions/tool-result.mjs +36 -0
  17. package/engine/actions/tool-visibility.mjs +248 -0
  18. package/engine/agent-capability.mjs +67 -0
  19. package/engine/anchor-match.mjs +14 -21
  20. package/engine/audience.d.mts +2 -0
  21. package/engine/audience.mjs +102 -0
  22. package/engine/branch.d.mts +4 -0
  23. package/engine/branch.mjs +61 -18
  24. package/engine/compaction-epoch.d.mts +25 -0
  25. package/engine/compaction-epoch.mjs +27 -4
  26. package/engine/compositions/source/local/filesystem-editor.yml +3 -3
  27. package/engine/compositions/source/local/instruction-hint.yml +2 -4
  28. package/engine/compositions/source/local/module-file-tools.yml +2 -0
  29. package/engine/compositions/source/local/module-import-tools.yml +4 -0
  30. package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
  31. package/engine/compositions/source/local/module-rule-tools.yml +5 -0
  32. package/engine/compositions/source/local/module-variable-tools.yml +5 -0
  33. package/engine/compositions/source/local/present-as.yml +26 -0
  34. package/engine/compositions/source/local/tool-config-engine.yml +2 -3
  35. package/engine/compositions/source/local/tool-search.yml +11 -0
  36. package/engine/condition-facts.mjs +84 -0
  37. package/engine/conditions/anchor.mjs +63 -23
  38. package/engine/conditions/availability.mjs +1 -1
  39. package/engine/conditions/composite.mjs +3 -3
  40. package/engine/conditions/count.mjs +42 -11
  41. package/engine/conditions/evaluation.mjs +74 -19
  42. package/engine/conditions/examples.mjs +2 -1
  43. package/engine/conditions/index.d.mts +15 -0
  44. package/engine/conditions/index.mjs +23 -9
  45. package/engine/conditions/operators.mjs +4 -0
  46. package/engine/conditions/phase.mjs +9 -3
  47. package/engine/conditions/preset.mjs +1 -1
  48. package/engine/conditions/session.mjs +8 -2
  49. package/engine/conditions/subject.mjs +144 -65
  50. package/engine/conditions/text.mjs +26 -8
  51. package/engine/conditions/variable.mjs +83 -0
  52. package/engine/context-activation.d.mts +11 -0
  53. package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
  54. package/engine/delivery.d.mts +2 -0
  55. package/engine/delivery.mjs +93 -0
  56. package/engine/discovery.d.mts +38 -0
  57. package/engine/discovery.mjs +146 -0
  58. package/engine/env-safety.mjs +14 -0
  59. package/engine/executor.d.mts +28 -0
  60. package/engine/executor.mjs +290 -205
  61. package/engine/fillers.mjs +39 -8
  62. package/engine/fixed-registration.mjs +12 -0
  63. package/engine/history.mjs +71 -0
  64. package/engine/instruction-blocks.d.mts +13 -0
  65. package/engine/instruction-blocks.mjs +21 -0
  66. package/engine/instruction-files.d.mts +5 -0
  67. package/engine/instruction-files.mjs +97 -0
  68. package/engine/instruction-hint.mjs +69 -103
  69. package/engine/interpolate.d.mts +7 -0
  70. package/engine/interpolate.mjs +44 -13
  71. package/engine/layers.mjs +163 -227
  72. package/engine/machine-paths.mjs +40 -0
  73. package/engine/module-file-tools.mjs +8 -0
  74. package/engine/module-import-tools.mjs +10 -0
  75. package/engine/module-manifest-tools.mjs +8 -0
  76. package/engine/module-rule-tools.mjs +10 -0
  77. package/engine/module-variable-tools.mjs +10 -0
  78. package/engine/order.d.mts +14 -0
  79. package/engine/order.mjs +0 -3
  80. package/engine/path-safety.d.mts +2 -0
  81. package/engine/path-safety.mjs +58 -0
  82. package/engine/pre-step-source.d.mts +69 -0
  83. package/engine/pre-step-source.mjs +103 -0
  84. package/engine/present-as.mjs +77 -0
  85. package/engine/promotion-pool.d.mts +9 -0
  86. package/engine/promotion-pool.mjs +69 -0
  87. package/engine/record.mjs +2 -0
  88. package/engine/rule-engine.mjs +3 -2
  89. package/engine/rule-facts.d.mts +11 -0
  90. package/engine/rule-facts.mjs +60 -0
  91. package/engine/rule-runtime.d.mts +46 -0
  92. package/engine/rule-runtime.mjs +98 -13
  93. package/engine/rule-spec.d.mts +106 -0
  94. package/engine/rule-spec.mjs +301 -27
  95. package/engine/run-code-env.mjs +73 -19
  96. package/engine/schema.mjs +165 -243
  97. package/engine/sdk-strip.mjs +2 -2
  98. package/engine/service-names.d.mts +10 -0
  99. package/engine/service-names.mjs +10 -0
  100. package/engine/shared.d.mts +21 -0
  101. package/engine/shared.mjs +46 -13
  102. package/engine/skill-search.mjs +125 -53
  103. package/engine/source-kind.d.mts +9 -0
  104. package/engine/source-kind.mjs +16 -0
  105. package/engine/strategy-ids.mjs +5 -0
  106. package/engine/template-evaluation.d.mts +16 -0
  107. package/engine/template-evaluation.mjs +178 -0
  108. package/engine/template-frame.d.mts +16 -0
  109. package/engine/template-frame.mjs +69 -0
  110. package/engine/tool-config-engine.mjs +38 -34
  111. package/engine/tool-definition.d.mts +4 -0
  112. package/engine/tool-definition.mjs +5 -1
  113. package/engine/tool-git-bash.mjs +5 -1
  114. package/engine/tool-scope.mjs +22 -0
  115. package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
  116. package/engine/trigger.mjs +15 -270
  117. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  118. package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
  119. package/engine/vendor/yaml/package.json +1 -1
  120. package/lib/client.js +8312 -6724
  121. package/lib/client.js.map +1 -1
  122. package/lib/index.d.mts +16 -2120
  123. package/lib/index.mjs +26914 -5816
  124. package/modules/ponytail/module.yml +83 -203
  125. package/modules/ptc-mode/module.yml +45 -0
  126. package/modules/soul/assets/core-hard.md +6 -0
  127. package/modules/soul/assets/criteria.md +12 -0
  128. package/modules/soul/assets/echo.md +12 -0
  129. package/modules/soul/assets/layers.md +11 -0
  130. package/modules/soul/assets/ledger.md +9 -0
  131. package/modules/soul/assets/manifest.yml +20 -0
  132. package/modules/soul/assets/reference/00-layers.md +27 -0
  133. package/modules/soul/assets/reference/01-criteria.md +32 -0
  134. package/modules/soul/assets/reference/02-echo.md +158 -0
  135. package/modules/soul/assets/reference/03-vital.md +183 -0
  136. package/modules/soul/assets/reference/04-manifest.json +106 -0
  137. package/modules/soul/assets/reference/05-doctor.md +67 -0
  138. package/modules/soul/assets/reference/06-reference.md +189 -0
  139. package/modules/soul/assets/reference/07-bundle.json +431 -0
  140. package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
  141. package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
  142. package/modules/soul/assets/reference/10-echo-anatomy.md +100 -0
  143. package/modules/soul/assets/reference/11-self.md +80 -0
  144. package/modules/soul/assets/reference/12-ledger.md +159 -0
  145. package/modules/soul/assets/reference/README.md +10 -0
  146. package/modules/soul/assets/templates/agents-template.md +6 -0
  147. package/modules/soul/assets/vital.md +9 -0
  148. package/modules/soul/module.yml +311 -0
  149. package/modules/superpowers/module.yml +122 -0
  150. package/modules/tool-surface/module.yml +51 -47
  151. package/package.json +37 -40
  152. package/skills/dsh-module/SKILL.md +67 -147
  153. package/skills/dsh-module/reference.md +113 -86
  154. package/templates/10-pre-step.yml +2 -3
  155. package/templates/14-first-turn-anchor.yml +10 -14
  156. package/templates/15-guide-auto.yml +32 -28
  157. package/templates/16-custom-fallback.yml +33 -21
  158. package/templates/20-system-section.yml +1 -4
  159. package/templates/50-llm-stream.yml +4 -10
  160. package/templates/60-tool-pipeline.yml +12 -10
  161. package/templates/63-append-context.yml +4 -6
  162. package/templates/65-turn-stop.yml +2 -2
  163. package/templates/67-inbox-prepend.yml +2 -2
  164. package/templates/70-subagent-maintenance.yml +9 -10
  165. package/templates/75-subagent-end.yml +2 -2
  166. package/templates/80-tool-surface.yml +41 -57
  167. package/templates/tools/40-delegate-module-import.yml +22 -0
  168. package/templates/tools/50-delegate-module-rules.yml +14 -0
  169. package/templates/tools/60-delegate-module-variables.yml +17 -0
  170. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  171. package/engine/actions/decision.mjs +0 -54
  172. package/engine/actions/guard.mjs +0 -80
  173. package/engine/actions/sdk-strip.mjs +0 -30
  174. package/engine/character-tools.mjs +0 -8
  175. package/engine/classify-task.mjs +0 -53
  176. package/engine/compositions/source/local/character-tools.yml +0 -5
  177. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  178. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  179. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  180. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  181. package/engine/condition.mjs +0 -96
  182. package/engine/declared-triggers.mjs +0 -93
  183. package/engine/session-var-tools.mjs +0 -8
  184. package/engine/session-vars.mjs +0 -54
  185. package/engine/st-macros.d.mts +0 -22
  186. package/engine/st-macros.mjs +0 -208
  187. package/engine/st-render.mjs +0 -109
  188. package/engine/st-world-book.d.mts +0 -10
  189. package/engine/subagent-tool-policy-core.mjs +0 -400
  190. package/engine/subagent-tool-policy.mjs +0 -352
  191. package/engine/trigger-spec.mjs +0 -173
  192. package/engine/world-book-tools.mjs +0 -8
  193. package/lib/migrate-rules.d.mts +0 -1
  194. package/lib/migrate-rules.mjs +0 -610
  195. package/lib/module-package-DqJZaE71.mjs +0 -2
  196. package/lib/module-package-q2gDRu9H.mjs +0 -2869
  197. package/lib/rule-builder-DMrJten6.mjs +0 -14767
  198. package/templates/61-deny-tools-presentation.yml +0 -27
  199. package/templates/62-guard-deny-tools.yml +0 -20
  200. package/templates/policies/legacy-defaults.yml +0 -34
  201. package/templates/tools/40-delegate-character.yml +0 -15
  202. package/templates/tools/50-delegate-worldbook.yml +0 -15
  203. package/templates/tools/60-delegate-session-var.yml +0 -15
  204. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -1,124 +1,68 @@
1
- import { NAME_LIST, createMask, labelOf, requireString } from './shared.mjs'
2
- import { sessionEvents } from '../shared.mjs'
3
-
4
1
  /**
5
- * 动态白名单:从**本会话已持久化的** `tool/call` 事件里回收解锁名单,填入 `into`。
6
- *
7
- * 语义对齐原 `tool-bootstrap.mjs` 的 `unlockedFor(session)`:某个发现工具(如
8
- * `dev_tool_search`)上一次调用时把要解锁的工具名写进了自己的调用参数,本函数把它读回来
9
- * 填入 allow,于是**解锁能跨请求保留**——没有它,解锁是一次性的(当次请求用完即被裁掉)。
2
+ * assembly —— 只改装配结果的 sections / contexts。
10
3
  *
11
- * 判据刻意保守,坏数据只影响该条、绝不上抛:
12
- * - 只认 `event.type === 'tool/call'` 且 `event.data.name` **精确等于**声明里的 `tool`;
13
- * - `arguments` 是模型产出的任意字符串,`JSON.parse` 必须 try/catch;解析出的必须是对象;
14
- * - 只取 `key` 指向的**字符串数组**里的字符串项,非字符串项逐项丢弃,不展开对象、不 eval;
15
- * - 任一步不成立就跳过该条事件,其余事件照常累积;全程无异常(读事件本身除外,由调用方兜)。
4
+ * 工具可见面(目录、SDK 正文、执行闸)已收进 `tool-visibility`:同一个意图不再分散到
5
+ * 多个动作。`target.tools` 在这里显式拒绝,避免旧声明「编译通过但静默不生效」。
16
6
  *
17
- * `tool` / `key` **按参数传入**而不是向外层读取——这个模块会被多个 assembly 动作各挂一次,
18
- * 任何模块级可变状态都会让它们互相覆盖。
7
+ * 语义保持原样:同层 sections 的 keep 与 remove 互斥;contexts 的 clear 与 add/remove 互斥;
8
+ * 异常一律返回未改动的装配(装配失败不得让会话失去系统段或上下文)。
19
9
  */
20
- function fillUnlocked(into, session, tool, key, warnOnce, plugin, label) {
21
- into.clear()
22
- let events
23
- try {
24
- events = sessionEvents(session)
25
- } catch (error) {
26
- warnOnce(`${plugin}: assembly action ${label}: reading session events for allowFrom failed: ${String(error?.message ?? error)}`)
27
- return
28
- }
29
- for (const event of events) {
30
- if (event?.type !== 'tool/call' || event?.data?.name !== tool) continue
31
- const raw = event.data.arguments
32
- const parsed = typeof raw === 'string' ? safeParse(raw) : raw
33
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) continue
34
- const value = parsed[key]
35
- if (!Array.isArray(value)) continue
36
- for (const item of value) if (typeof item === 'string' && item.length > 0) into.add(item)
37
- }
38
- }
39
10
 
40
- function safeParse(text) {
41
- try {
42
- return JSON.parse(text)
43
- } catch {
44
- return undefined
45
- }
46
- }
11
+ import { NAME_LIST, requireString } from './shared.mjs'
12
+ import { interpolateVariables } from '../interpolate.mjs'
13
+
14
+ const record = value => value !== null && typeof value === 'object' && !Array.isArray(value)
47
15
 
48
16
  export function prepareAssembly(action, plugin) {
49
17
  const target = action.target
50
- if (target === null || typeof target !== 'object' || Array.isArray(target)) {
51
- throw new TypeError(`${plugin}: assembly requires a target object — { tools } / { sections } / { contexts }`)
18
+ if (!record(target)) {
19
+ throw new TypeError(`${plugin}: assembly requires a target object — { sections } / { contexts }`)
52
20
  }
53
- const toolsSource = target.tools
54
- const allowFrom = toolsSource?.allowFrom
55
- let allowExtra
56
- if (allowFrom !== undefined) {
57
- if (allowFrom === null || typeof allowFrom !== 'object' || Array.isArray(allowFrom)) {
58
- throw new TypeError(`${plugin}: assembly.tools.allowFrom must be an object — { tool, key }`)
59
- }
60
- requireString(allowFrom.tool, 'assembly.tools.allowFrom.tool', plugin)
61
- requireString(allowFrom.key, 'assembly.tools.allowFrom.key', plugin)
62
- // deny 是黑名单、动态项只做加法,同声明的语义不可解释(且 allowFrom 已经覆盖不了 deny
63
- // 的名字到底该不该出现),挂载期直接拒,而不是运行期猜。
64
- if (Array.isArray(toolsSource.deny) && toolsSource.deny.length > 0) {
65
- throw new TypeError(`${plugin}: assembly.tools cannot combine deny with allowFrom — 动态项只做加法,与黑名单语义冲突`)
66
- }
67
- allowExtra = new Set()
21
+ if (target.tools !== undefined) {
22
+ throw new TypeError(`${plugin}: assembly.target.tools 已移除 — 工具可见面改用 tool-visibility 动作(allow/deny/discovery)`)
68
23
  }
69
- const tools = toolsSource === undefined ? undefined : createMask(toolsSource, 'assembly.tools', plugin, allowExtra)
70
24
  const sectionsAdd = target.sections?.add
71
25
  const sectionsRemove = NAME_LIST.parse(target.sections?.remove, plugin, 'assembly.sections.remove')
72
- // 白名单(2026-09-22 用户拍板「扩动作库」):`personaSectionsOnly` 一类的语义是
73
- // `filter(白名单)`,而 remove 是黑名单——未列名的第三方/晚到段不会被它删掉。
74
- // 两种语义**互斥**:同时声明即挂载期 fail loud,而不是静默二选一。
75
26
  const sectionsKeep = NAME_LIST.parse(target.sections?.keep, plugin, 'assembly.sections.keep')
76
- if (sectionsKeep !== undefined && sectionsRemove !== undefined) {
77
- throw new TypeError(`${plugin}: assembly target.sections cannot combine keep with remove — 一种是白名单、一种是黑名单,同时声明语义不明`)
27
+ const sectionsAppend = target.sections?.append
28
+ if (sectionsAppend !== undefined) {
29
+ if (!Array.isArray(sectionsAppend)) throw new TypeError(`${plugin}: assembly.sections.append must be an array of { name, text, once? }`)
30
+ for (const entry of sectionsAppend) {
31
+ if (!record(entry)) throw new TypeError(`${plugin}: assembly.sections.append entries must be objects`)
32
+ for (const key of Object.keys(entry)) {
33
+ if (!['name', 'text', 'once'].includes(key)) throw new TypeError(`${plugin}: assembly.sections.append[] unknown field ${JSON.stringify(key)}`)
34
+ }
35
+ requireString(entry.name, 'assembly.sections.append[].name', plugin)
36
+ requireString(entry.text, 'assembly.sections.append[].text', plugin)
37
+ if (entry.once !== undefined && typeof entry.once !== 'boolean') throw new TypeError(`${plugin}: assembly.sections.append[].once must be boolean`)
38
+ if (sectionsRemove?.has(entry.name)) throw new TypeError(`${plugin}: assembly.sections.append target ${JSON.stringify(entry.name)} cannot also be removed`)
39
+ }
78
40
  }
79
41
  const contextsAdd = target.contexts?.add
80
42
  const contextsRemove = NAME_LIST.parse(target.contexts?.remove, plugin, 'assembly.contexts.remove')
43
+ if (target.contexts?.clear !== undefined && typeof target.contexts.clear !== 'boolean') {
44
+ throw new TypeError(`${plugin}: assembly.contexts.clear must be boolean`)
45
+ }
81
46
  const contextsClear = target.contexts?.clear === true
82
47
  for (const [key, list] of [['sections.add', sectionsAdd], ['contexts.add', contextsAdd]]) {
83
48
  if (list === undefined) continue
84
49
  if (!Array.isArray(list)) throw new TypeError(`${plugin}: assembly.${key} must be an array of { name, text }`)
85
50
  for (const entry of list) {
86
- if (entry === null || typeof entry !== 'object') throw new TypeError(`${plugin}: assembly.${key} entries must be objects`)
51
+ if (!record(entry)) throw new TypeError(`${plugin}: assembly.${key} entries must be objects`)
87
52
  requireString(entry.name, `assembly.${key}[].name`, plugin)
88
53
  requireString(entry.text, `assembly.${key}[].text`, plugin)
89
54
  }
90
55
  }
56
+ if (contextsClear && (contextsAdd !== undefined || contextsRemove !== undefined)) {
57
+ throw new TypeError(`${plugin}: assembly.contexts.clear cannot combine with add or remove — clear 已清空全部条目,同时声明语义不明`)
58
+ }
59
+ if (sectionsKeep !== undefined && sectionsRemove !== undefined) {
60
+ throw new TypeError(`${plugin}: assembly.sections cannot combine keep with remove — 一种是白名单、一种是黑名单,同时声明语义不明`)
61
+ }
91
62
  return (_ctx, { warnOnce, on, collect, take }) => collect(on('system-prompt/assemble', async (assembly, context, next) => {
92
- // Downstream errors propagate untouched; only this action's own logic is guarded.
93
63
  const assembled = await next()
94
64
  try {
95
- if (typeof action.match === 'function' && action.match(context, assembled) !== true) return assembled
96
65
  let result = assembled
97
- if (tools !== undefined && Array.isArray(result.tools)) {
98
- // 每轮重算解锁名单:本轮装配要反映**本会话至今**的全部解锁调用。
99
- if (allowExtra !== undefined) {
100
- fillUnlocked(allowExtra, context?.session ?? context?.agent?.session, allowFrom.tool, allowFrom.key, warnOnce, plugin, labelOf(action))
101
- }
102
- // keep 名单兜底(2026-09-22 用户拍板,**可选**):判据与原 `tool-bootstrap` 逐条对齐——
103
- // 原模块是 `missing = required.filter(t => !available.has(t))`
104
- // 且 `missing.length > 0` 即放弃裁剪,也就是「**任意一个** 名单工具缺失」而不是
105
- // 「一个都不存在」(对拍实测:目录 `[bash,read]` + 名单 `[bash,str_replace_editor]` 时
106
- // 原模块给完整目录,只判"全缺"会裁成 `[bash]`)。
107
- // 声明写 `requireMatch: true` 才启用;**不写就照名单裁**(与 `tool-filter.mjs` 一致)。
108
- const available = new Set(result.tools.map((tool) => tool?.name).filter((name) => typeof name === 'string'))
109
- // 必需名单 = 静态 allow ∪ 本轮解锁项:解锁项同样是「本该存在」的工具,缺了就 fail-open。
110
- const required = tools.allow === undefined && allowExtra === undefined
111
- ? undefined
112
- : new Set([...(tools.allow ?? []), ...(allowExtra ?? [])])
113
- const missingRequired = tools.requireMatch && required !== undefined
114
- && [...required].some((name) => !available.has(name))
115
- if (missingRequired) {
116
- warnOnce(`${plugin}: assembly action ${labelOf(action)}: a requireMatch tool is absent — exposing the full tool set (fail-open)`)
117
- } else {
118
- const kept = result.tools.filter((tool) => !tools.blocks(tool?.name))
119
- if (kept.length !== result.tools.length) result = { ...result, tools: kept }
120
- }
121
- }
122
66
  if (Array.isArray(result.sections) && (sectionsAdd !== undefined || sectionsRemove !== undefined || sectionsKeep !== undefined)) {
123
67
  const kept = sectionsKeep !== undefined
124
68
  ? result.sections.filter((section) => sectionsKeep.has(section?.name))
@@ -129,6 +73,38 @@ export function prepareAssembly(action, plugin) {
129
73
  result = { ...result, sections: sectionsAdd === undefined ? kept : [...kept, ...sectionsAdd] }
130
74
  }
131
75
  }
76
+ if (Array.isArray(result.sections) && sectionsAppend !== undefined && sectionsAppend.length > 0) {
77
+ const seen = new Set()
78
+ let nextSections = result.sections
79
+ for (const entry of sectionsAppend) {
80
+ const index = nextSections.findIndex((section) => section?.name === entry.name)
81
+ if (index < 0) {
82
+ warnOnce(`${plugin}: assembly.sections.append target ${JSON.stringify(entry.name)} not found; skipped`)
83
+ continue
84
+ }
85
+ const section = nextSections[index]
86
+ if (typeof section?.text !== 'string') {
87
+ warnOnce(`${plugin}: assembly.sections.append target ${JSON.stringify(entry.name)} has no text; skipped`)
88
+ continue
89
+ }
90
+ const rendered = interpolateVariables(entry.text, {}, context?.agent?.session)
91
+ if (rendered.trim().length === 0 || /\{\{[^}]+\}\}/.test(rendered)) {
92
+ warnOnce(`${plugin}: assembly.sections.append target ${JSON.stringify(entry.name)} rendered empty; skipped`)
93
+ continue
94
+ }
95
+ if (entry.once !== false) {
96
+ const key = `${entry.name}\u0000${rendered}`
97
+ // once 覆盖同一装配内的重复动作/条目;已有贡献者追加过同一段正文时也不重复。
98
+ if (seen.has(key) || section.text.includes(rendered)) continue
99
+ seen.add(key)
100
+ }
101
+ const text = section.text.length === 0 ? rendered : `${section.text}\n${rendered}`
102
+ if (text === section.text) continue
103
+ if (nextSections === result.sections) nextSections = [...result.sections]
104
+ nextSections[index] = { ...section, text }
105
+ }
106
+ if (nextSections !== result.sections) result = { ...result, sections: nextSections }
107
+ }
132
108
  if (Array.isArray(result.contexts)) {
133
109
  if (contextsClear) {
134
110
  if (result.contexts.length > 0) result = { ...result, contexts: [] }
@@ -141,9 +117,9 @@ export function prepareAssembly(action, plugin) {
141
117
  }
142
118
  }
143
119
  }
144
- return result !== assembled && take(context) ? result : assembled
120
+ return result !== assembled ? result : assembled
145
121
  } catch (error) {
146
- warnOnce(`${plugin}: assembly action ${labelOf(action)} failed, exposing the full assembly: ${String(error?.message ?? error)}`)
122
+ warnOnce(`${plugin}: assembly action failed, keeping the assembled sections/contexts: ${String(error?.message ?? error)}`)
147
123
  return assembled
148
124
  }
149
125
  }))
@@ -6,69 +6,97 @@ export const ACTION_DEGRADE = Object.freeze({
6
6
  })
7
7
 
8
8
  /**
9
- * 九类动作的声明:**合法事件通道** + **降级语义** + 触发时机。
10
- * `events` 是动作允许注册的官方事件(注册到声明外的事件在挂载期 fail loud);
11
- * `services` 是它允许触碰的宿主服务方法(同一纪律的文档面)。
9
+ * 十类动作的声明:**合法事件通道** + **降级语义** + 触发时机 + **声明字段**。
10
+ * `events` 是动作允许注册的官方事件:经 `on(...)` 注册的动作(`channelBinder`)注册到声明外的
11
+ * 事件在挂载期 fail loud;`inject-text` 不经 `channelBinder`,它的真实通道由
12
+ * `actionExecutionPoint`(派生自 `schema.mjs` 每层的 `channel`)决定——下面那行 `events`
13
+ * 对它只是**文档面**(零校验、零消费),判断层是否可注入不要以它为准;
14
+ * `services` 是它允许触碰的宿主服务方法(同一纪律的文档面);
15
+ * `fields` 是该类动作自己的声明键(白名单写在这里**一份**,`rule-spec.mjs` 的未知键
16
+ * 检查从这里派生)。通用键 `id/kind/channelOrder/waterfallPosition/maxPerTurn` 由
17
+ * {@link ACTION_FIELDS} 统一补上;`if`/`then`/`else` 刻意不在名单里——带 `kind`
18
+ * 的动作写分支键会被编译期点名拒绝(`expandActions` 只认无 `kind` 的分支节点)。
12
19
  */
13
20
  export const ACTION_KINDS = Object.freeze({
14
21
  'inject-text': {
15
22
  title: '注入文本',
16
- events: ['agent/pre-step', 'session/event', 'system-prompt/assemble', 'agent/request', 'llm/stream', 'tools/pre-execute', 'tools/post-execute', 'agent/turn-stopping', 'subagent/start', 'subagent/end'],
23
+ fields: ['config', 'options'],
24
+ events: ['agent/pre-step', 'session/event', 'system-prompt/assemble', 'agent/turn-stopping', 'subagent/start', 'subagent/end'],
17
25
  services: ['systemPrompt.section', 'systemPrompt.context', 'systemPrompt.variable'],
18
26
  degrade: ACTION_DEGRADE.keep,
19
27
  timing: '按配置声明的 layer 落到该层官方通道;pre-step 在 agent/pre-step,system-section/runtime-context 在注册期 + 官方 assembly。',
20
- note: '复用九层注册通道,不新开通道;显式空正文 = 不注册(empty 情形)。`options` 原样交给该层的既有接线(pre-step 执行器的 `prepend` = 声明的位置策略);`session/event` 是 pre-step 执行器自带的会话态旁听,不是第二条注入通道。',
28
+ note: '复用既有注入通道(见 schema 的 `injectionLayers`),不新开通道;工具链的裁决与追加上下文走 `tool-gate` / `tool-result` / `append-context`,请求参数走 `request-params`,模型流走 `stream-output`。显式空正文不注册;`session/event` 是 pre-step 执行器自带的会话态旁听,不是第二条注入通道。',
21
29
  },
22
30
  assembly: {
23
31
  title: '改装配',
32
+ fields: ['target', 'match'],
24
33
  events: ['system-prompt/assemble'],
25
34
  services: [],
26
35
  degrade: ACTION_DEGRADE.exposeAll,
27
36
  timing: 'system-prompt/assemble 的下游结算之后(`await next()` 之后改,装配事实已就绪)。',
28
37
  note: '失败时返回未改动的装配 = 对 tools 即暴露完整目录;不改 global,只作用于本次 assembly。',
29
38
  },
30
- decision: {
31
- title: '裁决',
32
- events: ['tools/pre-execute', 'tools/post-execute'],
39
+ 'tool-gate': {
40
+ title: '工具裁决',
41
+ fields: ['decision', 'reason'],
42
+ events: ['tools/pre-execute'],
33
43
  services: [],
34
44
  degrade: ACTION_DEGRADE.keep,
35
- timing: 'pre 相在工具执行之前返回 allow|deny|ask;post 相在结果结算之后返回 accept|replace|block。',
36
- note: '异常与条件未命中一律 `next()`(放行/接受),绝不让裁决 bug 卡死调用。',
45
+ timing: 'pre 相在工具执行之前返回 allow|deny|ask。',
46
+ note: '异常与条件未命中一律 `next()`(放行),绝不让裁决 bug 卡死调用;判据只看本相真正读的字段。',
37
47
  },
38
- 'append-context': {
39
- title: '追加上下文与续跑',
40
- events: ['tools/post-execute', 'agent/turn-stopping'],
48
+ 'tool-result': {
49
+ title: '工具结果处理',
50
+ fields: ['action', 'text'],
51
+ events: ['tools/post-execute'],
41
52
  services: [],
42
53
  degrade: ACTION_DEGRADE.keep,
43
- timing: 'mode=context 在 post-execute 追加 durable user 通知;mode=continue 在 agent/turn-stopping steer 一步。',
44
- note: '续跑共用引擎的续跑预算(每轮 1 次 / 每会话 3 次),无正文时不追加(empty 情形);绝不伪造 assistant 角色。',
54
+ timing: 'post 相在结果结算之后返回 accept|replace|block。',
55
+ note: '异常一律 `next()`(接受);`text` 仅 replace / block 使用,accept 时被忽略。',
45
56
  },
46
- guard: {
47
- title: '执行层 guard',
48
- events: ['system-prompt/assemble'],
49
- services: ['tools.guard', 'tools.restrict'],
50
- degrade: ACTION_DEGRADE.silent,
51
- timing: '首次 assembly 时按 agent scope 惰性注册一次(预设切换/重绑后按新的 agent.ctx 重注册)。',
52
- note: '最终拒绝层:注册在 agent.ctx 上(工具本层与晚到工具也被裁决),命中返回拒绝原因;不注册全局 guard。guard 自身不吞异常——异常即失败,绝不退化成"放行"。',
57
+ 'append-context': {
58
+ title: '追加上下文',
59
+ fields: ['text', 'match'],
60
+ events: ['tools/post-execute'],
61
+ services: [],
62
+ degrade: ACTION_DEGRADE.keep,
63
+ timing: '在 post-execute 追加 durable user 通知。',
64
+ note: '无正文时不追加;绝不伪造 assistant 角色。',
53
65
  },
54
- 'sdk-strip': {
55
- title: '裁 SDK 声明文本',
66
+
67
+ 'tool-visibility': {
68
+ title: '工具可见面',
69
+ fields: ['allow', 'deny', 'requireMatch', 'discovery'],
56
70
  events: ['system-prompt/assemble'],
57
- services: [],
71
+ services: ['tools.schemas', 'tools.restrict', 'tools.guard', 'tools.register'],
58
72
  degrade: ACTION_DEGRADE.keep,
59
- timing: 'system-prompt/assemble 下游结算之后,改写名为 tools:sdk 的段文本。',
60
- note: '只删不增、保守失败:形态异常/自校验失败一律原样返回;空名单零开销。仅是呈现补救,执行边界在 guard。',
73
+ timing: 'system-prompt/assemble 的下游结算之后:按当前条件重算目录、SDK 正文与执行闸;条件翻转时撤销上一轮注册的闸。',
74
+ note: '一份声明拥有工具的可见面:allow/deny 名单、requireMatch、发现工具。名单语义对齐官方 '
75
+ + 'ToolRestriction 的 and/or(同时给时先按 allow 保留、再按 deny 剔除)。deny 走 guard(本层与晚到工具)'
76
+ + '与 restrict(继承的全局工具);allow 只有 restrict 能覆盖全局工具,本层注册无法在执行层「只保留」——'
77
+ + '这是官方 restrict 的边界,不是本动作的取舍。',
61
78
  },
62
79
  'request-params': {
63
80
  title: '改模型请求参数',
81
+ fields: ['patch', 'unset', 'replace'],
64
82
  events: ['agent/request'],
65
83
  services: [],
66
84
  degrade: ACTION_DEGRADE.keep,
67
85
  timing: 'agent/request 的下游结算之后(`await next()` 拿到已冻结的 LlmCallConfig 再改写)。',
68
- note: '与既有 promptConfigs 的 agent-request 层共用 applyAgentRequestParams;只改命中 scope/agent 的请求,删键仅删"本声明注入的那个值"。',
86
+ note: '请求改写的唯一入口;删键仅删本声明指定的那个值。',
87
+ },
88
+ 'stream-output': {
89
+ title: '控制模型输出流',
90
+ fields: ['mode', 'text'],
91
+ events: ['llm/stream'],
92
+ services: [],
93
+ degrade: ACTION_DEGRADE.keep,
94
+ timing: 'llm/stream 下游调用前决定透传或替换。',
95
+ note: '替换输出为合法文本 chunk,不改冻结的请求;空正文透传。',
69
96
  },
70
97
  'inbox-prepend': {
71
98
  title: '前置收件箱消息',
99
+ fields: ['target', 'text', 'match'],
72
100
  events: ['agent/inbox/inserted'],
73
101
  services: ['agent.inbox.prepend'],
74
102
  degrade: ACTION_DEGRADE.silent,
@@ -78,6 +106,7 @@ export const ACTION_KINDS = Object.freeze({
78
106
  },
79
107
  'pre-step-filter': {
80
108
  title: '过滤 pre-step 注入消息',
109
+ fields: ['sources', 'keepKinds', 'blockPlugins'],
81
110
  events: ['agent/pre-step'],
82
111
  services: [],
83
112
  degrade: ACTION_DEGRADE.exposeAll,
@@ -90,3 +119,11 @@ export const ACTION_KINDS = Object.freeze({
90
119
  + '**永不吞上下文**:异常一律返回未过滤的 decision(`expose-all`)。',
91
120
  },
92
121
  })
122
+
123
+ /** 十类动作共用键:身份、同点定位与每轮预算。 */
124
+ const COMMON_ACTION_FIELDS = ['id', 'kind', 'channelOrder', 'waterfallPosition', 'maxPerTurn']
125
+
126
+ /** 每类动作的允许键集合(通用键 + `ACTION_KINDS[kind].fields`)。 */
127
+ export const ACTION_FIELDS = Object.freeze(Object.fromEntries(
128
+ Object.entries(ACTION_KINDS).map(([kind, meta]) => [kind, new Set([...COMMON_ACTION_FIELDS, ...meta.fields])]),
129
+ ))
@@ -1,159 +1,14 @@
1
1
  /**
2
2
  * strategies — 引擎内容策略绑定(config.resolve)。
3
- * 内容策略: static / placeholder / first-turn-anchor / guide-auto / anchor-notice / world-book。
3
+ * 内容策略: static / placeholder / context-activation。
4
4
  * 策略参数全部来自 config.params(由 preset.yml 单一配置源下发),引擎只负责组装。
5
5
  * 仍支持 strategyDir 懒加载自定义模板策略。
6
6
  */
7
7
 
8
- import { extractText } from '../shared.mjs'
9
- import { MATCH_LOGIC, createAnchorMatcher } from '../anchor-match.mjs'
10
- import { createTaskClassifier } from '../classify-task.mjs'
11
8
  import { createPlaceholderResolver } from '../fillers.mjs'
12
- import { createAnchorState } from '../conditions/anchor.mjs'
13
9
 
14
10
  const name = 'prompt-config-engine'
15
11
 
16
- /**
17
- * first-turn-anchor:首条真实用户消息后的一次性任务锚点。
18
- * 正则与锚句文本全部来自 config.params(由 preset.yml 单一配置源下发)。
19
- */
20
- function createFirstTurnAnchorResolver(config) {
21
- const useCustom = config.params?.useCustom === true
22
- const customText = typeof config.params?.text === 'string' ? config.params.text : ''
23
- const buildPattern = typeof config.params?.buildPattern === 'string' ? config.params.buildPattern : ''
24
- const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
25
- const firstTurnBuild = typeof config.params?.firstTurnBuild === 'string' ? config.params.firstTurnBuild : ''
26
- const firstTurnInspect = typeof config.params?.firstTurnInspect === 'string' ? config.params.firstTurnInspect : ''
27
- const firstTurnDeep = typeof config.params?.firstTurnDeep === 'string' ? config.params.firstTurnDeep : ''
28
- const classifier = createTaskClassifier({ buildPattern, complexPattern })
29
- return ({ messages }) => {
30
- if (useCustom) {
31
- const text = customText.trim()
32
- return text.length > 0 ? { text } : null
33
- }
34
- if (!classifier.ready || (firstTurnBuild.length === 0 && firstTurnInspect.length === 0 && firstTurnDeep.length === 0)) {
35
- return null
36
- }
37
- const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
38
- if (userIndex < 0) return null
39
- const taskText = extractText(messages[userIndex])
40
- if (taskText.length === 0) return null
41
- const kind = classifier.classify(taskText)
42
- const anchor = kind === 'complex' ? firstTurnDeep : kind === 'build' ? firstTurnBuild : firstTurnInspect
43
- return anchor.length > 0 ? { text: anchor } : null
44
- }
45
- }
46
-
47
- /**
48
- * guide-auto:晋升后每轮用户消息后的弱/深度引导。
49
- * 正则与引导文本全部来自 config.params(由 preset.yml 单一配置源下发)。
50
- * 复杂任务判定 fallback 复用锚定功能的 complexPattern(guideComplexPattern
51
- * 冗余副本已移除)——锚定与引导是独立功能,仅分类器共用。
52
- */
53
- function createGuideAutoResolver(config) {
54
- const useCustom = config.params?.useCustom === true
55
- const customText = typeof config.params?.text === 'string' ? config.params.text : ''
56
- const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
57
- const guideWeak = typeof config.params?.guideWeak === 'string' ? config.params.guideWeak : ''
58
- const guideDeep = typeof config.params?.guideDeep === 'string' ? config.params.guideDeep : ''
59
- const rawMinChars = config.params?.complexMinChars
60
- const complexMinChars = rawMinChars == null || rawMinChars === '' ? undefined : rawMinChars
61
- if (complexMinChars !== undefined && (!Number.isSafeInteger(complexMinChars) || complexMinChars < 0)) throw new TypeError(`${name}: guide-auto complexMinChars must be a non-negative integer`)
62
- // 复杂判定 fallback 复用锚定分类器(buildPattern 不参与引导判定)。
63
- const classifier = createTaskClassifier({ complexPattern })
64
- return ({ messages }) => {
65
- const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
66
- if (userIndex < 0) return null
67
- const text = extractText(messages[userIndex])
68
- if (text.length === 0) return null
69
- if (useCustom) {
70
- const guide = customText.trim()
71
- return guide.length > 0 ? { text: guide } : null
72
- }
73
- if (guideWeak.length === 0 && guideDeep.length === 0) return null
74
- return { text: ((complexMinChars !== undefined && text.length > complexMinChars) || classifier.isComplex(text)) ? guideDeep : guideWeak }
75
- }
76
- }
77
-
78
- /**
79
- * anchor-notice 只生成显式正文与来源说明;资格由规则条件单独负责。
80
- */
81
- function createAnchorNoticeResolver(config) {
82
- const promptText = config.texts.length > 0
83
- ? config.texts.join('\n\n')
84
- : (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
85
- // 确认词无内置默认(默认值归模板/预设):空 = 不做词确认,只走未命中兜底注入。
86
- const firstTurnWord = typeof config.params?.firstTurnWord === 'string' ? config.params.firstTurnWord : ''
87
- // 确认词集合:writePreset 派生的 anchorWords 优先(锚句信号词多词 prefix,任一命中即
88
- // 确认——deep 档 Let…/自定义锚句首词都覆盖);无 anchorWords 时用显式确认词。
89
- const anchorWords = Array.isArray(config.params?.anchorWords) && config.params.anchorWords.length > 0
90
- ? config.params.anchorWords.map(String).filter((word) => word.length > 0)
91
- : (firstTurnWord.length > 0 ? [firstTurnWord] : [])
92
- const inspect = createAnchorState({ keys: anchorWords })
93
-
94
- return ({ agent }) => {
95
- if (promptText === undefined) return null
96
- const { confirmed } = inspect(agent.session)
97
- return {
98
- text: promptText,
99
- source: {
100
- // v4 要求 kind 是生产者名:裸 `plugin` 是退役包装,会被会话格式校验拒绝。
101
- kind: `plugin:${config.id}`,
102
- plugin: config.id,
103
- form: 'notice',
104
- summary: confirmed
105
- ? (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」锚定确认后注入)` : 'prompt-tool 提示词(无确认词,锚定就绪后注入)')
106
- : (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」未确认,兜底注入)` : 'prompt-tool 提示词(无确认词,兜底注入)'),
107
- },
108
- }
109
- }
110
- }
111
-
112
- /**
113
- * world-book:世界书条目(角色卡 lorebook)。constant=true 恒注入(不扫 keys);
114
- * selective 条目扫描当前消息批文本,keys/secondaryKeys 按 selectiveLogic 组合
115
- * (any=任一命中 / all=副键全中 / not=副键全不中)。匹配经 anchor-match 引擎。
116
- */
117
- function createWorldBookResolver(config) {
118
- if (config.params?.stWorldBook) return ({ stWorldBookSelected }) => stWorldBookSelected === true ? { text: config.texts.join('\n\n') } : null
119
- const constant = config.params?.constant === true
120
- // selectiveLogic:ST world_info_logic 0=AND_ANY 1=NOT_ALL 2=NOT_ANY 3=AND_ALL。
121
- const rawLogic = config.params?.selectiveLogic
122
- const logic = rawLogic === 3
123
- ? MATCH_LOGIC.ALL
124
- : (rawLogic === 1
125
- ? MATCH_LOGIC.NOT
126
- : (rawLogic === 2 ? MATCH_LOGIC.NOT_ANY : MATCH_LOGIC.ANY))
127
- const matcher = createAnchorMatcher({
128
- keys: Array.isArray(config.params?.keys) ? config.params.keys : [],
129
- secondaryKeys: Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : [],
130
- caseSensitive: config.params?.caseSensitive === true,
131
- wholeWords: config.params?.wholeWords === true,
132
- // 三态透传:undefined=ST 语义自动检测(/regex/ 或含特殊字符键按正则);
133
- // 存量配置显式 true/false 仍强制对应行为。
134
- useRegex: config.params?.useRegex,
135
- logic,
136
- })
137
- // ST 语义:constant 或无任何键的条目恒注入(always active)。
138
- const hasAnyKey = (Array.isArray(config.params?.keys) ? config.params.keys : [])
139
- .some((key) => String(key).trim().length > 0)
140
- || (Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : [])
141
- .some((key) => String(key).trim().length > 0)
142
- const promptText = config.texts.length > 0
143
- ? config.texts.join('\n\n')
144
- : (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
145
- return ({ messages }) => {
146
- if (promptText === undefined) return null
147
- if (constant || !hasAnyKey) return { text: promptText }
148
- const haystack = (Array.isArray(messages) ? messages : [])
149
- .map((message) => extractText(message))
150
- .filter((text) => text.length > 0)
151
- .join('\n')
152
- if (haystack.length === 0) return null
153
- return matcher.scan(haystack).active ? { text: promptText } : null
154
- }
155
- }
156
-
157
12
  /**
158
13
  * 为归一化后的提示词配置绑定策略 resolve(策略状态随提示词配置对象,不随 apply)。
159
14
  * strategyDir:外部策略模块目录(相对本文件 URL)。未声明时只允许内置策略。
@@ -161,14 +16,13 @@ function createWorldBookResolver(config) {
161
16
  export function bindResolver(config, strategyDir) {
162
17
  switch (config.strategy) {
163
18
  case 'placeholder': return createPlaceholderResolver(config)
164
- case 'first-turn-anchor': return createFirstTurnAnchorResolver(config)
165
- case 'guide-auto': return createGuideAutoResolver(config)
19
+ case 'guide-auto':
166
20
  case 'custom-fallback':
167
- throw new TypeError(`${name}: migrate custom-fallback to an anchor condition and anchor-notice content`)
21
+ case 'first-turn-anchor':
168
22
  case 'anchor-notice':
169
- return createAnchorNoticeResolver(config)
170
- case 'world-book':
171
- return createWorldBookResolver(config)
23
+ throw new TypeError(`${name}: retired strategy ${config.strategy}; use rule conditions and static content`)
24
+ case 'context-activation':
25
+ return ({ contextActivationSelected }) => contextActivationSelected === true ? { text: config.texts.join('\n\n') } : null
172
26
  case 'static': {
173
27
  const texts = config.texts
174
28
  const patch = config.templatePatch ?? {}
@@ -198,6 +52,8 @@ export function bindResolver(config, strategyDir) {
198
52
  if (typeof make !== 'function') {
199
53
  throw new TypeError(`${name}: strategy module ${moduleUrl.href} must export createResolver(config)`)
200
54
  }
55
+ // 每次求值重建工厂产物(不缓存 closure):缓存会同时改变闭包状态跨调用、文件热刷新、
56
+ // 跨会话串扰三项可观察行为——模块本身仍按上面缓存一次,只省 import。
201
57
  return make(config)(args)
202
58
  }
203
59
  }
@@ -1,10 +1,14 @@
1
1
  export const ACTION_EXAMPLES = {
2
2
  'inject-text': { config: { id: 'notice', layer: 'pre-step', text: '' } },
3
3
  assembly: { target: {} },
4
- decision: { phase: 'pre', decision: 'allow' },
5
- 'append-context': { mode: 'context', text: '' },
6
- guard: { mask: { deny: [] } },
7
- 'sdk-strip': { mask: { deny: [] } },
4
+ // 工具链拆成两个动作:pre 相的 tool-gate 与 post 相的 tool-result。种子取引擎缺省,
5
+ // 中性值不替用户选业务值:`reason: ''` 与缺键同义,都落引擎缺省原因(用 `||`)。
6
+ 'tool-gate': { decision: 'allow', reason: '' },
7
+ 'tool-result': { action: 'accept', text: '' },
8
+ 'append-context': { text: '' },
9
+ 'stream-output': { mode: 'pass', text: '' },
10
+ // 种子给一个合法名单:空名单无法表达「剔哪些工具」(createMask 会拒绝)。
11
+ 'tool-visibility': { deny: ['web_search'] },
8
12
  'request-params': { patch: {} },
9
13
  'inbox-prepend': { target: 'next-turn', text: '' },
10
14
  'pre-step-filter': { blockPlugins: [] },
@@ -1,5 +1,6 @@
1
1
  import { labelOf, requireString } from './shared.mjs'
2
2
  import { newMessageId } from '../shared.mjs'
3
+ import { isPluginKind, pluginKind } from '../source-kind.mjs'
3
4
 
4
5
  export function prepareInboxPrepend(action, plugin) {
5
6
  const label = labelOf(action)
@@ -17,17 +18,15 @@ export function prepareInboxPrepend(action, plugin) {
17
18
  // (对拍实测:声明路径多插 2 条)。
18
19
  if (agent?.session === undefined) return
19
20
  if (typeof agent?.inbox?.prepend !== 'function') return
20
- // v4 起插件来源的 kind 是生产者名(`plugin:<name>`);两种形态一起认,否则换代后
21
- // 「插件来源消息永不再次前置」这道防自触发的闸会静默失效。
22
- if (typeof message?.source?.kind === 'string' && message.source.kind.startsWith('plugin:')) return
23
- if (message?.source?.kind === 'plugin') return
21
+ // 插件来源(v4 或旧日志)的判定收在 source-kind;漏掉会让防自触发闸静默失效。
22
+ if (isPluginKind(message?.source?.kind)) return
24
23
  if (typeof action.match === 'function' && action.match(agent, message) !== true) return
25
24
  if (!take({ agent })) return
26
25
  agent.inbox.prepend(target, {
27
26
  id: newMessageId(`action-${label}`),
28
27
  role: 'user',
29
28
  content: [{ type: 'text', text }],
30
- source: { kind: `plugin:${label}`, plugin: label, form: 'notice', summary: `${label} ${target}` },
29
+ source: { kind: pluginKind(label), plugin: label, form: 'notice', summary: `${label} ${target}` },
31
30
  })
32
31
  } catch (error) {
33
32
  warnOnce(`${plugin}: inbox-prepend action ${label} failed: ${String(error?.message ?? error)}`)