dsh-plugin-prompt-tool 2.0.1 → 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 (231) 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 +5 -7
  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 +23 -15
  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 +62 -19
  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 +4 -7
  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 +4 -4
  40. package/engine/conditions/count.mjs +43 -13
  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 +10 -5
  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 +165 -230
  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 +302 -28
  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 +47 -26
  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 +39 -35
  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 +37 -762
  123. package/lib/index.mjs +26919 -5791
  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 +40 -41
  152. package/skills/dsh-module/SKILL.md +119 -0
  153. package/skills/dsh-module/reference.md +185 -0
  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 +20 -0
  162. package/templates/64-pre-step-filter.yml +23 -0
  163. package/templates/65-turn-stop.yml +2 -2
  164. package/templates/67-inbox-prepend.yml +22 -0
  165. package/templates/70-subagent-maintenance.yml +9 -10
  166. package/templates/75-subagent-end.yml +2 -2
  167. package/templates/80-tool-surface.yml +41 -57
  168. package/templates/tools/40-delegate-module-import.yml +22 -0
  169. package/templates/tools/50-delegate-module-rules.yml +14 -0
  170. package/templates/tools/60-delegate-module-variables.yml +17 -0
  171. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  172. package/engine/actions/decision.mjs +0 -54
  173. package/engine/actions/guard.mjs +0 -80
  174. package/engine/actions/sdk-strip.mjs +0 -30
  175. package/engine/character-tools.mjs +0 -8
  176. package/engine/classify-task.mjs +0 -53
  177. package/engine/compositions/library/agent-instructions.yml +0 -9
  178. package/engine/compositions/library/command-goal.yml +0 -7
  179. package/engine/compositions/library/compaction.yml +0 -22
  180. package/engine/compositions/library/delegation-ptc.yml +0 -58
  181. package/engine/compositions/library/delegation.yml +0 -56
  182. package/engine/compositions/library/persistent-shell.yml +0 -50
  183. package/engine/compositions/library/planning.yml +0 -26
  184. package/engine/compositions/library/skill-filesystem-cordis.yml +0 -10
  185. package/engine/compositions/library/skill-filesystem.yml +0 -7
  186. package/engine/compositions/library/tool-ask-user.yml +0 -7
  187. package/engine/compositions/library/tool-bash.yml +0 -8
  188. package/engine/compositions/library/tool-cordis.yml +0 -7
  189. package/engine/compositions/library/tool-fs-search.yml +0 -9
  190. package/engine/compositions/library/tool-fs.yml +0 -7
  191. package/engine/compositions/library/tool-goal.yml +0 -7
  192. package/engine/compositions/library/tool-jobs.yml +0 -7
  193. package/engine/compositions/library/tool-plugin-manager-disabled.yml +0 -8
  194. package/engine/compositions/library/tool-plugin-manager.yml +0 -8
  195. package/engine/compositions/library/tool-present.yml +0 -7
  196. package/engine/compositions/library/tool-presentation.yml +0 -9
  197. package/engine/compositions/library/tool-pwsh.yml +0 -8
  198. package/engine/compositions/library/tool-skill.yml +0 -7
  199. package/engine/compositions/library/tool-todo.yml +0 -9
  200. package/engine/compositions/library/tool-web.yml +0 -10
  201. package/engine/compositions/source/local/character-tools.yml +0 -5
  202. package/engine/compositions/source/local/declared-triggers.yml +0 -12
  203. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  204. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  205. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  206. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  207. package/engine/condition.mjs +0 -96
  208. package/engine/declared-triggers.mjs +0 -93
  209. package/engine/session-var-tools.mjs +0 -8
  210. package/engine/session-vars.mjs +0 -54
  211. package/engine/st-macros.d.mts +0 -22
  212. package/engine/st-macros.mjs +0 -208
  213. package/engine/st-render.mjs +0 -109
  214. package/engine/st-world-book.d.mts +0 -10
  215. package/engine/subagent-tool-policy-core.mjs +0 -400
  216. package/engine/subagent-tool-policy.mjs +0 -352
  217. package/engine/trigger-spec.mjs +0 -173
  218. package/engine/world-book-tools.mjs +0 -8
  219. package/lib/migrate-rules.d.mts +0 -1
  220. package/lib/migrate-rules.mjs +0 -39
  221. package/lib/module-package-CHPFweqK.mjs +0 -2
  222. package/lib/module-package-Cnd1_mwx.mjs +0 -2872
  223. package/lib/paths-CyP0IkaM.d.mts +0 -1390
  224. package/lib/preset-transfer.d.mts +0 -2
  225. package/lib/preset-transfer.mjs +0 -3
  226. package/lib/rules-migration-DJ6vASB5.mjs +0 -15334
  227. package/templates/policies/legacy-defaults.yml +0 -34
  228. package/templates/tools/40-delegate-character.yml +0 -15
  229. package/templates/tools/50-delegate-worldbook.yml +0 -15
  230. package/templates/tools/60-delegate-session-var.yml +0 -15
  231. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
package/engine/layers.mjs CHANGED
@@ -1,25 +1,25 @@
1
1
  /**
2
- * layers — 非 pre-step 的五个官方层级接线。
2
+ * layers — 非 pre-step 的官方层级接线。
3
3
  * 所有注册都只作用于本插件提示词配置;单条失败 warnOnce 后继续。
4
4
  */
5
5
 
6
6
  import {
7
7
  MAX_TRACKED_SESSIONS,
8
- extractText,
9
8
  getService,
10
9
  isDelegated,
11
10
  keepDisposer,
11
+ matchesAudience,
12
12
  matchesModel,
13
13
  newMessageId,
14
- parseToolNames,
15
14
  sessionState,
16
15
  } from './shared.mjs'
17
- import { KNOWN_STRATEGIES } from './schema.mjs'
18
- import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue } from './interpolate.mjs'
19
- import { getSessionVar, sessionVarsSnapshot } from './session-vars.mjs'
20
- import { conditionHit, lastAssistantText, subagentTextOf, toolArgsText } from './condition.mjs'
16
+ import { pluginKind } from './source-kind.mjs'
17
+ import { INJECTION_LAYERS, KNOWN_STRATEGIES, SESSION_VARIABLES_DISABLED } from './schema.mjs'
18
+ import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue, isReservedInterpolationName } from './interpolate.mjs'
19
+ import { renderTemplate } from './template-frame.mjs'
21
20
  import { compareConfigSequence, compareTextPlacement } from './order.mjs'
22
- import { ruleFrame, ruleMatches } from './conditions/evaluation.mjs'
21
+ import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
22
+ import { textRegistrationName, identityStampOf, actionStamp, injectedMessage, findActionDelivery } from './delivery.mjs'
23
23
 
24
24
  const name = 'prompt-config-engine'
25
25
  // 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
@@ -33,11 +33,14 @@ const subagentEndDeliveries = new WeakMap()
33
33
  * keep=官方变量通道按 assembly 求值的名字,静态 pass 命中即原样保留引用。 */
34
34
  function configText(config, registries, warnOnce, context) {
35
35
  const registry = registries.get(config)
36
- if (typeof config.renderSt === 'function') {
37
- return officialChannelText(config.renderSt(context?.agent, [], warnOnce, context), `${config.layer} ${config.id}`, registry, warnOnce)
36
+ if (config.params?.templateEvaluation === true) {
37
+ try { return officialChannelText(renderTemplate(config, context, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce }), `${config.layer} ${config.id}`, registry, warnOnce) }
38
+ catch (error) { warnOnce(`${name}: template ${config.id} failed: ${String(error?.message ?? error)}`); return '' }
38
39
  }
40
+ const variables = config[SESSION_VARIABLES_DISABLED] === true ? {} : config.texts.some(text => text.includes('{{'))
41
+ ? config.variableStore?.read(config.sourceModuleId).effective ?? config.variables : config.variables
39
42
  const text = config.texts
40
- .map((item) => interpolateVariables(item, config.variables, context?.agent?.session, registry?.protect))
43
+ .map((item, index) => interpolateVariables(item, variables, context?.agent?.session, registry?.protect, `${config.id}#${index}`))
41
44
  .filter((item) => item.length > 0)
42
45
  .join('\n\n')
43
46
  return officialChannelText(text, `${config.layer} ${config.id}`, registry, warnOnce)
@@ -76,7 +79,8 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
76
79
  const reserved = new Set()
77
80
  const facts = new Set()
78
81
  for (const config of configs) {
79
- if (config.params?.stMacros === true) continue
82
+ // 持久值在各正文 provider 被调用时读取,使后卡看到前卡刚提交的变量。
83
+ if (config.params?.templateEvaluation === true || config.variableStore !== undefined) continue
80
84
  const referenced = new Set()
81
85
  for (const text of config.texts) {
82
86
  for (const match of String(text).matchAll(NAME_REFERENCE_RE)) referenced.add(match[1])
@@ -93,14 +97,14 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
93
97
  for (const [config, referenced] of references) {
94
98
  const registry = registries.get(config)
95
99
  for (const name of referenced) {
96
- const declared = Object.hasOwn(config.variables, name)
100
+ const declared = Object.hasOwn(config.variables, name) || config.variableStore !== undefined && !isReservedInterpolationName(name)
97
101
  const isFact = !declared && RUNTIME_FACTS.has(name.toLowerCase())
98
102
  if (!declared && !isFact) continue
99
103
  let official = isFact ? name.toLowerCase() : OFFICIAL_NAME_RE.test(name) ? name : officialAliasOf(name)
100
104
  try {
101
105
  // 内容绑定包含局部命名空间:会话覆盖也可以再引用其中的变量。
102
106
  // 纯事实不属于任何配置的内容命名空间,所有大小写变体复用规范名。
103
- const signature = isFact ? `fact:${official}` : JSON.stringify([name, Object.entries(config.variables).sort(([a], [b]) => a.localeCompare(b))])
107
+ const signature = isFact ? `fact:${official}` : JSON.stringify([config.sourceModuleId, name, config.variableStore === undefined ? Object.entries(config.variables).sort(([a], [b]) => a.localeCompare(b)) : null])
104
108
  let binding = bindings.get(signature)
105
109
  if (binding === undefined) {
106
110
  if (usedNames.has(official) || (!isFact && facts.has(official)) || (official !== name && !isFact && reserved.has(official))) {
@@ -110,12 +114,13 @@ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
110
114
  }
111
115
  usedNames.add(official)
112
116
  const source = isFact ? name.toLowerCase() : name
113
- const declaredValue = declared ? String(config.variables[name] ?? '') : undefined
117
+ const variablesDisabled = config[SESSION_VARIABLES_DISABLED] === true
114
118
  keep(systemPrompt.variable(official, (context) => {
115
119
  const session = context?.agent?.session
116
- const override = getSessionVar(session, source)
117
- const value = override !== undefined ? String(override) : declaredValue ?? runtimeFactValue(source, session) ?? ''
118
- const expanded = interpolateVariables(value, { ...config.variables, ...sessionVarsSnapshot(session) }, session)
120
+ const variables = variablesDisabled ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables
121
+ const value = Object.hasOwn(variables, source) ? String(variables[source]) : runtimeFactValue(source, session) ?? ''
122
+ // 变量值内的 {{pick}} 同样要有位置身份(配置 id + 声明名),否则同会话下多个变量的首个 pick 撞值。
123
+ const expanded = interpolateVariables(value, variables, session, undefined, `variable:${config.sourceModuleId}:${config.id}:${name}`)
119
124
  return officialChannelText(expanded, `variable ${official}`, { alias: new Map(), registered: new Set() }, warnOnce)
120
125
  }), `${name}: official prompt variable ${official}`)
121
126
  binding = official
@@ -141,16 +146,14 @@ function rewriteOfficialAliases(text, alias) {
141
146
  /** agent/request 与 tools/* 层的作用域过滤。 */
142
147
  export function matchesAgentScope(config, agent) {
143
148
  if (agent === undefined) return true
144
- const delegated = isDelegated(agent.session)
145
- if (config.audience === 'main' && delegated) return false
146
- if (config.audience === 'subagent' && !delegated) return false
149
+ if (!matchesAudience(config.audience, isDelegated(agent.session) ? 'subagent' : 'main')) return false
147
150
  return matchesModel(config.modelScope, agent.options?.model)
148
151
  }
149
152
 
150
153
  /**
151
154
  * 官方插值通道(system-section / runtime-context)出口清洗:本项目解析后仍残留的
152
155
  * 引用一律剥离,官方严格插值因此看不到未注册引用(既不抛错也不留字面)。
153
- * 只在本项目的宽容解析之后调用;其他层(pre-step / agent-request / tool-pipeline)
156
+ * 只在本项目的宽容解析之后调用;其他层(pre-step / agent-request)
154
157
  * 不经官方插值,保持原有宽容语义。
155
158
  */
156
159
  function officialChannelText(text, label, registry, warnOnce) {
@@ -171,36 +174,27 @@ function textLayerGroups(configs) {
171
174
  const base = previous?.[0]
172
175
  if (config.mergeMode === 'merged' && base?.mergeMode === 'merged'
173
176
  && config.sourceModuleId === base.sourceModuleId
177
+ && (config.params?.complete === true) === (base.params?.complete === true)
178
+ && (config.params?.suppressRuntimeContext === true) === (base.params?.suppressRuntimeContext === true)
174
179
  && config.position === base.position && config.order === base.order) previous.push(config)
175
180
  else groups.push([config])
176
181
  }
177
182
  return groups
178
183
  }
179
184
 
180
- /** 官方同 order 按 name 排序:默认名编码安全整数序号,显式名仍由作者拥有。 */
181
- function textRegistrationName(config, field) {
182
- const explicit = config.params?.[field]
183
- if (typeof explicit === 'string' && explicit.length > 0) return explicit
184
- if (config.sourceModuleId === undefined) return config.id
185
- // 非负安全整数最多 16 位,跨过四位文件前缀后仍保持数值顺序。
186
- const sequence = String(config.sequence).padStart(16, '0')
187
- return `prompt-tool:${sequence}:${config.sourceModuleId}:${config.id}`
188
- }
189
-
190
185
  /** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
191
- function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
192
- const disposers = []
186
+ function wireSystemSections(ctx, configs, registry, warnOnce, keep, ordered) {
193
187
  const systemPrompt = getService(ctx, 'systemPrompt')
194
188
  if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
195
189
  if (configs.length > 0) warnOnce(`${name}: systemPrompt service unavailable — system-section configs skipped`)
196
- return disposers
190
+ return
197
191
  }
198
192
  // 人设段(deployment:persona-prefix/suffix)由官方 @deepseek-ai/dsh-persona 行注册
199
193
  // (preset.yml 顶层 persona 段驱动);本层只处理其余 system-section 配置。
200
194
  for (const group of textLayerGroups(configs)) {
201
195
  const base = group[0]
202
196
  try {
203
- const dynamic = group.some((config) => config.audience != null || typeof config.renderSt === 'function'
197
+ const dynamic = group.some((config) => config.audience != null || config.params?.templateEvaluation === true || config.variableStore !== undefined
204
198
  || config.texts.some(text => /\{\{\s*(?:random|pick|roll|chance)(?=\s|:|\})/i.test(text)))
205
199
  const groupText = dynamic ? '' : group.map((config) => configText(config, registry, warnOnce)).filter((item) => item.length > 0).join('\n\n')
206
200
  if (!dynamic && groupText.length === 0) continue
@@ -211,12 +205,14 @@ function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
211
205
  .filter((item) => item.length > 0)
212
206
  .join('\n\n')
213
207
  : groupText
208
+ const entryName = textRegistrationName(base, 'sectionName')
214
209
  keep(systemPrompt.section({
215
- name: textRegistrationName(base, 'sectionName'),
210
+ name: entryName,
216
211
  order: base.order,
217
212
  text,
218
213
  ...(base.params?.complete === true ? { complete: true } : {}),
219
214
  }), `${name}: section ${base.id}`)
215
+ ordered.sections.push(orderedEntry(entryName, base))
220
216
  // suppressRuntimeContext 抑制该 scope 的动态 runtime-context 快照(多段重复调用幂等)。
221
217
  if (base.params?.suppressRuntimeContext === true) {
222
218
  keep(systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
@@ -225,7 +221,6 @@ function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
225
221
  warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
226
222
  }
227
223
  }
228
- return disposers
229
224
  }
230
225
 
231
226
  async function resolvedContextText(ctx, config, context, registry, warnOnce) {
@@ -233,14 +228,19 @@ async function resolvedContextText(ctx, config, context, registry, warnOnce) {
233
228
  const session = agent?.session
234
229
  const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
235
230
  if (context.signal?.aborted || resolved == null) return ''
236
- const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
237
- const rendered = config.texts.length > 0 ? interpolateVariables(config.texts.join('\n\n'), variables, session)
238
- : typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
231
+ const variables = { ...(config[SESSION_VARIABLES_DISABLED] === true ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables), ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
232
+ if (config.params?.templateEvaluation === true) {
233
+ const texts = config.texts.length ? config.texts : [resolved.text ?? '']
234
+ return officialChannelText(renderTemplate({ ...config, texts }, context, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce }), `runtime-context ${config.id}`, registry, warnOnce)
235
+ }
236
+ // 模板位置身份 = 配置 id(texts 合并与 resolved.text 是同一位置的两个互斥分支);缺省会退化成 [会话, 出现序号]。
237
+ const rendered = config.texts.length > 0 ? interpolateVariables(config.texts.join('\n\n'), variables, session, undefined, config.id)
238
+ : typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session, undefined, config.id) : ''
239
239
  return officialChannelText(rendered, `runtime-context ${config.id}`, registry, warnOnce)
240
240
  }
241
241
 
242
242
  /** 条件化文本保留官方排序占位;仅在本次真实 assembly 填充,不缓存 context 身份。 */
243
- function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
243
+ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on, ordered) {
244
244
  if (!configs.length) return []
245
245
  const systemPrompt = getService(ctx, 'systemPrompt')
246
246
  if (systemPrompt === undefined) { warnOnce(`${name}: systemPrompt unavailable`); return [] }
@@ -254,6 +254,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
254
254
  const entryName = textRegistrationName(base, section ? 'sectionName' : 'contextName')
255
255
  keep(systemPrompt[section ? 'section' : 'context']({ name: entryName, order: base.order, text: marker }), `${name}: conditional ${entryName}`)
256
256
  bindings.push({ group, name: entryName, collection: section ? 'sections' : 'contexts' })
257
+ ordered[section ? 'sections' : 'contexts'].push(orderedEntry(entryName, base))
257
258
  }
258
259
  let active = true
259
260
  keep(() => { active = false }, `${name}: conditional text lifecycle`)
@@ -265,7 +266,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
265
266
  entry.text = ''
266
267
  const blocks = []
267
268
  for (const config of binding.group) {
268
- if (!active || context.signal?.aborted || !ruleMatches(config.rule, frame) || !matchesAgentScope(config, context.agent)) continue
269
+ if (!active || context.signal?.aborted || !actionMatches(config, frame) || !matchesAgentScope(config, context.agent)) continue
269
270
  try {
270
271
  const text = config.layer === 'runtime-context' && (config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy))
271
272
  ? await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
@@ -281,7 +282,7 @@ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
281
282
  }
282
283
 
283
284
  /** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
284
- function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
285
+ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on, ordered) {
285
286
  const disposers = []
286
287
  const systemPrompt = getService(ctx, 'systemPrompt')
287
288
  if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
@@ -295,17 +296,20 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
295
296
  for (const group of textLayerGroups(staticConfigs)) {
296
297
  const base = group[0]
297
298
  try {
298
- const dynamic = group.some(config => typeof config.renderSt === 'function'
299
+ const dynamic = group.some(config => config.params?.templateEvaluation === true || config.variableStore !== undefined
299
300
  || config.texts.some(text => /\{\{\s*(?:random|pick|roll|chance)(?=\s|:|\})/i.test(text)))
300
301
  const render = context => group.map(config => configText(config, registry, warnOnce, context)).filter(item => item.length > 0).join('\n\n')
301
302
  const text = dynamic ? render : render()
302
303
  if (!dynamic && text.length === 0) continue
304
+ const entryName = textRegistrationName(base, 'contextName')
303
305
  keep(systemPrompt.context({
304
- name: textRegistrationName(base, 'contextName'),
306
+ name: entryName,
305
307
  order: base.order,
306
308
  text,
307
309
  }), `${name}: context ${base.id}`)
310
+ ordered.contexts.push(orderedEntry(entryName, base))
308
311
  } catch (error) {
312
+ if (base.linkedDelivery) throw error
309
313
  warnOnce(`${name}: runtime-context config ${base.id} failed: ${String(error?.message ?? error)}`)
310
314
  }
311
315
  }
@@ -329,6 +333,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
329
333
  text: slotText,
330
334
  }), `${name}: context ${config.id}`)
331
335
  registered.set(contextName, config)
336
+ ordered.contexts.push(orderedEntry(contextName, config))
332
337
  } catch (error) {
333
338
  warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
334
339
  }
@@ -352,91 +357,6 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
352
357
  return disposers
353
358
  }
354
359
 
355
- /**
356
- * agent/request 载荷的唯一改写实现(B3 T2 第 (7) 类动作与既有 `agent-request`
357
- * 层**共用这一份**,不另写一套请求改写)。三种语义:
358
- * - `patch`:对下游已解析的 LlmCallConfig 做浅合并;
359
- * - `replace: true`:整体替换为 `patch`(不叠加 base,故 `unset` 无比较对象);
360
- * - `unset`:**按值条件删键** —— 仅当合并结果里的值**恰好等于**声明值时删除该键
361
- * (`Object.is` 比较)。这来自原 `tool-bootstrap` 剥离 `bootstrapMaxTokens` 的语义
362
- * (「仅当解析后的 maxTokens 恰好等于本声明注入的那个值时才删」;声明侧见
363
- * `test/engine/declarations/tool-bootstrap.yml` 声明 4 的 `do.unset`),不是"存在即删"
364
- * ——模型或其它插件设置的同名参数不会被删。
365
- * 浅合并做不到删键,故删键必须走显式通道。
366
- * @param params 提示词配置的 `params`(或动作的等价声明)。
367
- * @param base 下游 `next()` 已解析的请求配置。
368
- * @returns 改写后的请求配置;无 unset 命中时零额外分配。
369
- */
370
- export function applyAgentRequestParams(params, base) {
371
- const patch = params?.patch !== null && typeof params?.patch === 'object' && !Array.isArray(params.patch)
372
- ? params.patch
373
- : {}
374
- if (params?.replace === true) return { ...patch }
375
- const merged = { ...base, ...patch }
376
- const unset = params?.unset
377
- if (unset === null || typeof unset !== 'object' || Array.isArray(unset)) return merged
378
- let result
379
- for (const [key, expected] of Object.entries(unset)) {
380
- // 每个键都对着**合并结果**判定:一旦分配过副本,后续键不得跳过比较。
381
- if (!Object.is(merged[key], expected)) continue
382
- if (result === undefined) result = { ...merged }
383
- delete result[key]
384
- }
385
- return result ?? merged
386
- }
387
-
388
- /** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
389
- function wireAgentRequests(ctx, configs, warnOnce, on) {
390
- if (configs.length === 0) return []
391
- const ordered = configs.some(config => config.sequence !== undefined) ? configs : [...configs].reverse()
392
- return [on('agent/request', async (payload, next, invocation) => {
393
- let result = await next()
394
- const frame = invocation ?? ruleFrame('agent/request', [payload], warnOnce)
395
- // 模块请求在 next 返回后按序合并;无文件来源的独立调用保持原回栈顺序。
396
- for (const config of ordered) {
397
- try {
398
- if (ruleMatches(config.rule, frame) && matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
399
- } catch (error) {
400
- warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
401
- }
402
- }
403
- return result
404
- })]
405
- }
406
-
407
- /** 把流替换为提示词配置文本的最小合法 chunk 序列。 */
408
- async function* replacedStream(text) {
409
- yield { type: 'block-start', index: 0, blockType: 'text' }
410
- yield { type: 'text-delta', index: 0, text }
411
- yield { type: 'block-end', index: 0, block: { type: 'text', text } }
412
- }
413
-
414
- /** llm-stream:pass 透传;replace 用提示词配置文本替代整个模型流。 */
415
- function wireLlmStreams(ctx, configs, warnOnce, on) {
416
- const disposers = []
417
- for (const config of configs) {
418
- disposers.push(on('llm/stream', (options, next, invocation) => {
419
- try {
420
- const frame = invocation ?? ruleFrame('llm/stream', [options], warnOnce)
421
- const agent = options?.sessionId === undefined ? undefined : getService(ctx, 'agents')?.get?.(options.sessionId)
422
- frame.subject.agent = agent
423
- if (!ruleMatches(config.rule, frame)) return next()
424
- const mode = config.params?.mode ?? 'pass'
425
- if (mode === 'replace' && config.texts.length > 0 && matchesModel(config.modelScope, options?.model)) {
426
- return replacedStream(config.texts.join('\n\n'))
427
- }
428
- return next()
429
- } catch (error) {
430
- warnOnce(`${name}: llm-stream config ${config.id} failed: ${String(error?.message ?? error)}`)
431
- return next()
432
- }
433
- }))
434
- }
435
- return disposers
436
- }
437
-
438
- /** 条件判定的匹配器与取文本逻辑由 condition.mjs 承载(pre-step 与其他层共用)。 */
439
-
440
360
  /**
441
361
  * 插件来源的 user 消息:与 anchor-turn / progress-reminder 同一形状。
442
362
  *
@@ -451,15 +371,17 @@ export function pluginMessage(prefix, text, summary) {
451
371
  id: newMessageId(prefix),
452
372
  role: 'user',
453
373
  content: [{ type: 'text', text }],
454
- source: { kind: `plugin:${name}`, plugin: name, form: 'notice', summary },
374
+ source: { kind: pluginKind(name), plugin: name, form: 'notice', summary },
455
375
  }
456
376
  }
457
377
 
458
378
  /** 层级文本:按声明变量插值后按空行拼接;不走官方插值通道(非 system-section)。 */
459
379
  function layerText(config, agent, warnOnce) {
460
380
  try {
381
+ if (config.params?.templateEvaluation === true) return renderTemplate(config, { agent }, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce })
382
+ const variables = config[SESSION_VARIABLES_DISABLED] === true ? {} : config.variableStore?.read(config.sourceModuleId).effective ?? config.variables
461
383
  return config.texts
462
- .map((item) => interpolateVariables(item, config.variables, agent?.session))
384
+ .map((item, index) => interpolateVariables(item, variables, agent?.session, undefined, `${config.sourceModuleId}:${config.id}#${index}`))
463
385
  .filter((item) => item.length > 0)
464
386
  .join('\n\n')
465
387
  } catch (error) {
@@ -468,56 +390,10 @@ function layerText(config, agent, warnOnce) {
468
390
  }
469
391
  }
470
392
 
471
- /** tool-pipeline:pre-execute 判定、execute 包装、post-execute 结果替换/阻断。 */
472
- function wireToolPipelines(ctx, configs, warnOnce, on) {
473
- const disposers = []
474
- for (const config of configs) {
475
- const names = parseToolNames(config.params?.toolNames)
476
- const matchesTool = (exec) => names.length === 0 || names.includes(exec?.name)
477
- disposers.push(on('tools/pre-execute', async (exec, next, invocation) => {
478
- try {
479
- if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/pre-execute', [exec], warnOnce))) return next()
480
- if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
481
- if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments) })) return next()
482
- const decision = config.params?.preDecision ?? 'allow'
483
- if (decision === 'allow') return next()
484
- if (decision === 'deny') {
485
- return { kind: 'deny', reason: String(config.params?.denyReason ?? `${config.name}: denied by prompt config`) }
486
- }
487
- if (decision === 'ask') return { kind: 'ask' }
488
- return next()
489
- } catch (error) {
490
- warnOnce(`${name}: tool-pipeline(pre) config ${config.id} failed: ${String(error?.message ?? error)}`)
491
- return next()
492
- }
493
- }))
494
- disposers.push(on('tools/post-execute', async (exec, result, next, invocation) => {
495
- try {
496
- if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/post-execute', [exec, result], warnOnce))) return next()
497
- if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
498
- if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments), resultText: extractText(result) })) return next()
499
- const action = config.params?.postAction ?? 'accept'
500
- if (action === 'accept') return next()
501
- if (action === 'replace' && config.texts.length > 0) {
502
- return { kind: 'accept', content: [{ type: 'text', text: config.texts.join('\n\n') }] }
503
- }
504
- if (action === 'block') {
505
- return { kind: 'block', feedback: [{ type: 'text', text: config.texts.length > 0 ? config.texts.join('\n\n') : `${config.name}: blocked by prompt config` }] }
506
- }
507
- return next()
508
- } catch (error) {
509
- warnOnce(`${name}: tool-pipeline(post) config ${config.id} failed: ${String(error?.message ?? error)}`)
510
- return next()
511
- }
512
- }))
513
- }
514
- return disposers
515
- }
516
-
517
393
  /**
518
- * 轮次停止层的续跑上限:每轮 1 次、每会话 3 次,均为引擎常量。
519
- * 不暴露为配置——强制续跑失控会把会话卡在停不下来的循环里;官方桥在同一位置
520
- * 也只留了 TODO(stop-loop-guard) 而未实现上限。
394
+ * 轮次停止层(`turn-stop` 与续跑动作)的续跑上限:**每个来源(模块)**每轮 1 次、
395
+ * 每会话 3 次,均为引擎常量。不暴露为配置——强制续跑失控会把会话卡在停不下来的
396
+ * 循环里;官方桥在同一位置也只留了 TODO(stop-loop-guard) 而未实现上限。
521
397
  */
522
398
  export const TURN_STOP_MAX_PER_TURN = 1
523
399
  export const TURN_STOP_MAX_PER_SESSION = 3
@@ -526,8 +402,10 @@ export const TURN_STOP_MAX_PER_SESSION = 3
526
402
  export const TURN_STOP_MAX_TRACKED_TURNS = 8
527
403
 
528
404
  /**
529
- * 续跑预算(唯一实现):B3 T2 第 (4) 类动作的「续跑」与既有 turn-stop 层共用它,
405
+ * 续跑预算(唯一实现):所有 turn-stop 注入共用它,
530
406
  * 上限是引擎常量而不是配置——强制续跑失控会把会话卡在停不下来的循环里。
407
+ * **一份预算覆盖一个来源模块**(规则运行时按模块传入同一份),因此同模块的多条
408
+ * turn-stop 动作合计每轮 1 次、每会话 3 次。
531
409
  * 三步语义刻意与迁移前逐行一致:
532
410
  * entry(...) 取条目(含创建与超限轮次淘汰的副作用),在条件判定**之前**调用;
533
411
  * available(...) 只读判定,不落账;
@@ -537,24 +415,22 @@ export function createTurnStopBudget() {
537
415
  /**
538
416
  * 会话预算条目(`sessionState`:统一访问接口,策略逐条声明)。
539
417
  *
540
- * 键类型 `session.id`;淘汰策略超限 `clear()` 全清(与迁移前的 `sessionMapGet` 同档:
418
+ * 键类型 `session.id`;淘汰策略超限 `clear()` 全清(与迁移前的会话态记账同档:
541
419
  * 第 `MAX_TRACKED_SESSIONS` 个会话仍会清空整个容器);**不声明复位**——预算是纯增量、
542
420
  * 不可从事件流重建的计数(与 `progress-reminder` 同档),丢失时机必须与迁移前逐字一致,
543
421
  * compaction 也不重置它。
544
422
  *
545
- * 本函数**没有 ctx**:`createTurnStopBudget()` 是 B3 交付物的一部分,对外签名与
546
- * `entry/available/claim` 三个方法逐字不变(调用方见 actions.mjs 的 append-context 与
547
- * 本文件的 turn-stop 层)。`sessionState` 只在声明了 `reset` 时才读 ctx,因此这里传
423
+ * 本函数没有 ctx,只有 turn-stop 注入消费预算。
424
+ * `sessionState` 只在声明了 `reset` 时才读 ctx,因此这里传
548
425
  * `undefined` 合法且零开销(详见 shared.mjs 的 JSDoc 与 test/engine/session-state.test.mjs)。
549
426
  */
550
427
  const state = sessionState(undefined, () => ({ turns: new Map(), total: 0 }))
551
428
 
552
429
  const stateOf = (sessionId, turn) => {
553
430
  // 对外仍是 `(sessionId, turn)`:按 id 造一次性键宿主,`keyOf` 取的就是 `.id`,与旧的
554
- // `sessionMapGet(state, sessionId, …)` 写进同一个槽位、同一个 clear 时机。
431
+ // 记账写法写进同一个槽位、同一个 clear 时机。
555
432
  // 唯一差异在**无 id 的会话**(`sessionId === undefined`):`sessionState` 刻意不记账
556
- // (见 shared.mjs),旧写法把 `undefined` 当一个共享槽位。两个调用点(actions.mjs 的
557
- // append-context 与下方 turn-stop 层)都在 `entry()` 之前守卫 `session?.id === undefined`,
433
+ // (见 shared.mjs),下方 turn-stop 层在 `entry()` 前守卫 `session?.id === undefined`,
558
434
  // 故该路径不可达;这里兜底一个新条目,避免将来误用时抛 TypeError。
559
435
  const entry = state.get({ id: sessionId }) ?? { turns: new Map(), total: 0 }
560
436
  if (!Number.isFinite(turn)) return entry
@@ -579,11 +455,15 @@ export function createTurnStopBudget() {
579
455
  }
580
456
  }
581
457
 
582
- /** turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。 */
583
- function wireTurnStops(ctx, configs, warnOnce, on) {
458
+ /**
459
+ * turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。
460
+ * options.turnStopBudgets 由调用方传入时按同一份预算记账(规则运行时按模块一份),
461
+ * 缺省仍按 sourceModuleId 各建一份。
462
+ */
463
+ function wireTurnStops(ctx, configs, warnOnce, on, turnStopBudgets) {
584
464
  const disposers = []
585
465
  if (configs.length === 0) return disposers
586
- const budgets = new Map()
466
+ const budgets = turnStopBudgets ?? new Map()
587
467
 
588
468
  for (const config of configs) {
589
469
  const source = config.sourceModuleId ?? ''
@@ -592,18 +472,20 @@ function wireTurnStops(ctx, configs, warnOnce, on) {
592
472
  disposers.push(on('agent/turn-stopping', (payload = {}, _next, invocation) => {
593
473
  const { agent, turn } = payload
594
474
  try {
595
- if (!ruleMatches(config.rule, invocation ?? ruleFrame('agent/turn-stopping', [payload], warnOnce))) return
475
+ if (payload.signal?.aborted) return
476
+ if (!actionMatches(config, invocation ?? ruleFrame('agent/turn-stopping', [payload], warnOnce))) return
596
477
  const session = agent?.session
597
478
  if (session?.id === undefined || typeof agent.steer !== 'function') return
598
479
  if (!matchesAgentScope(config, agent)) return
480
+ if (config.dedupe === 'session' && (config.linkedDelivery ? findActionDelivery(session, config) : injectedMessage(config, session))) return
599
481
  const entry = budget.entry(session.id, turn)
600
482
  if (!budget.available(entry, turn)) return
601
- if (!conditionHit(config, { assistantText: lastAssistantText(session) })) return
602
483
  const text = layerText(config, agent, warnOnce)
603
484
  if (text.length === 0) return
604
485
  // 计数在 steer 之前落账:steer 抛错也不允许下一步重试越过预算。
605
486
  budget.claim(entry, turn)
606
- agent.steer(pluginMessage(`turn-stop-${config.id}`, text, `turn-stop ${config.id}`))
487
+ const message = pluginMessage(`turn-stop-${config.id}`, text, `turn-stop ${config.id}`)
488
+ agent.steer({ ...message, source: { ...message.source, ...identityStampOf(config), ...actionStamp(config, session) } })
607
489
  } catch (error) {
608
490
  warnOnce(`${name}: turn-stop config ${config.id} failed: ${String(error?.message ?? error)}`)
609
491
  }
@@ -645,7 +527,7 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
645
527
  const session = child?.session ?? getService(ctx, 'sessions')?.get?.(info.id)
646
528
  if (session === undefined) return
647
529
  const mainId = mainSessionForChild(ctx, info.id)
648
- // ponytail: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
530
+ // shortcut: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
649
531
  if (runs.size >= MAX_TRACKED_SESSIONS) runs.delete(runs.keys().next().value)
650
532
  runs.set(info.runId, { id: info.id, mainId, agent: child, session, model: child?.options?.model })
651
533
  }))
@@ -660,11 +542,9 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
660
542
  if (child === undefined) return
661
543
  const frame = invocation ?? ruleFrame('subagent/start', [info], warnOnce)
662
544
  frame.subject.agent = child
663
- const subagentText = subagentTextOf(info)
664
545
  for (const config of startConfigs) {
665
- if (!ruleMatches(config.rule, frame)) continue
546
+ if (!actionMatches(config, frame)) continue
666
547
  if (!matchesAgentScope(config, child)) continue
667
- if (!conditionHit(config, { subagentText })) continue
668
548
  if (typeof child.inject !== 'function') continue
669
549
  const text = layerText(config, child, warnOnce)
670
550
  if (text.length === 0) continue
@@ -682,17 +562,16 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
682
562
  const recorded = runs.get(info?.runId)
683
563
  runs.delete(info?.runId)
684
564
  if (recorded !== undefined && recorded.id !== info?.id) return
685
- const subagentText = subagentTextOf(info)
686
565
  const frame = invocation ?? ruleFrame('subagent/end', [info], warnOnce)
687
566
  frame.subject.agent = child ?? recorded?.agent
688
567
  frame.subject.session = child?.session ?? recorded?.session ?? getService(ctx, 'sessions')?.get?.(info?.id)
689
568
  frame.subject.model = child?.options?.model ?? recorded?.model
690
569
  for (const config of endConfigs) {
691
- if (!ruleMatches(config.rule, frame)) continue
570
+ if (!actionMatches(config, frame)) continue
692
571
  if (!matchesModel(config.modelScope, child?.options?.model ?? recorded?.model)) continue
693
- if (!conditionHit(config, { subagentText })) continue
694
572
  if (config.params?.action !== 'inject-main') {
695
- warnOnce(`${name}: subagent-end ${config.id} matched (observe only)`)
573
+ // 模板本意是逐次记录;命中本身已由装配实例的判定账本记 hit,这里不占告警名额。
574
+ ctx.logger?.info?.(`${name}: subagent-end ${config.id} matched (observe only)`)
696
575
  continue
697
576
  }
698
577
  if (typeof info?.runId !== 'string' || info.runId.length === 0) continue
@@ -708,7 +587,7 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
708
587
  if (delivered === undefined) { delivered = new Map(); subagentEndDeliveries.set(main.session, delivered) }
709
588
  const key = `${info.runId}:${config.id}`
710
589
  if (delivered.has(key)) continue
711
- // ponytail: 会话内保留最近的投递记录;超大历史改用持久事件索引时可取消此上限。
590
+ // shortcut: 会话内保留最近的投递记录;超大历史改用持久事件索引时可取消此上限。
712
591
  if (delivered.size >= MAX_TRACKED_SESSIONS) delivered.delete(delivered.keys().next().value)
713
592
  delivered.set(key, true)
714
593
  main.inject(pluginMessage(`subagent-end-${config.id}`, text, `subagent-end ${config.id}`))
@@ -721,43 +600,99 @@ function wireSubagentEvents(ctx, configs, warnOnce, on) {
721
600
  return disposers
722
601
  }
723
602
 
603
+ /** 注册名与序号:字段只在注册点算一次,重排器只消费这里的结果。 */
604
+ function orderedEntry(name, config) {
605
+ return { name, sequence: config.sequence ?? config.order ?? 0, order: config.order ?? 0 }
606
+ }
607
+
608
+ /**
609
+ * 实际注册名按 configOrder(sequence)排序;同序号保持注册序,同名只留一次。
610
+ * 名字来自注册点而不是事后从 configs 推导,避免 regular/conditional/placeholder 三条路径漂移。
611
+ */
612
+ function orderedNamesOf(entries) {
613
+ // configOrder 优先;同序号回到官方 order(而不是接线顺序),最后 stable 保留注册顺序。
614
+ return [...new Set([...entries]
615
+ .sort((left, right) => left.sequence - right.sequence || (left.order ?? 0) - (right.order ?? 0))
616
+ .map((entry) => entry.name))]
617
+ }
618
+
619
+ /**
620
+ * 官方先按 order/段名排好投递顺序;这里按 configOrder 把本插件注册的段/上下文
621
+ * 回填到它们原来的槽位。只换本插件段之间的位置:平台段与其他插件段占住的槽位不动。
622
+ */
623
+ function registerDeliveryOrder(ctx, keep, collection, names) {
624
+ if (names.length < 2) return
625
+ const rank = new Map(names.map((entryName, index) => [entryName, index]))
626
+ keep(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
627
+ const result = await next()
628
+ const list = result?.[collection]
629
+ if (!Array.isArray(list)) return result
630
+ const slots = []
631
+ for (let index = 0; index < list.length; index++) {
632
+ if (rank.has(list[index]?.name)) slots.push(index)
633
+ }
634
+ if (slots.length < 2) return result
635
+ const ordered = slots.map((index) => list[index]).sort((left, right) => rank.get(left.name) - rank.get(right.name))
636
+ if (slots.every((slot, index) => list[slot] === ordered[index])) return result
637
+ const nextList = [...list]
638
+ slots.forEach((slot, index) => { nextList[slot] = ordered[index] })
639
+ return { ...result, [collection]: nextList }
640
+ }, { prepend: true }), `${name}: ${collection} order`)
641
+ }
642
+
724
643
  /**
725
644
  * 把非 pre-step 提示词配置接入其声明的官方层级通道。
726
- * @returns 聚合 disposer:回收本次接线显式创建的 waterfall 监听器;段/上下文/
727
- * 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
645
+ * @param options.agent 当前 ctx 所属的真实 Agent;固定效果的受众在注册前匹配。
646
+ * @param options.turnStopBudgets 可选的续跑预算映射(键 = 模块 id,来源缺失用空串兜底);
647
+ * 规则运行时按模块传入同一份,使同模块的 turn-stop 动作共享上限。
648
+ * @returns 聚合 disposer:回收监听器及段/上下文/变量注册;后者也绑定 ctx fiber,重复释放幂等。
728
649
  */
729
650
  export function wireLayers(ctx, configs, warnOnce, options = {}) {
651
+ // 固定效果必须在目标 Agent 注册前筛选;空正文不能撤销官方 complete/suppress 声明。
652
+ configs = configs.filter(config => {
653
+ if (config.layer !== 'system-section' || config.audience == null
654
+ || config.params?.complete !== true && config.params?.suppressRuntimeContext !== true) return true
655
+ if (options.agent?.session === undefined) throw new TypeError(`${name}: fixed system-section ${config.id} with audience requires an explicit Agent in options.agent`)
656
+ return matchesAudience(config.audience, isDelegated(options.agent.session) ? 'subagent' : 'main')
657
+ })
730
658
  configs = [...configs].sort(compareConfigSequence)
659
+ // 没有注入通道的层(`tool-pipeline`)在这里没有分派:显式告警一次,别让 config 级入口静默丢弃。
660
+ for (const config of configs) {
661
+ if (!INJECTION_LAYERS.includes(config.layer)) warnOnce(`${name}: ${config.id}: layer ${String(config.layer)} 没有注入通道,已跳过(工具链的裁决请用 decision / append-context)`)
662
+ }
731
663
  const owned = []
732
664
  const keep = (dispose, label) => {
733
665
  if (typeof dispose !== 'function') return
734
666
  let active = true
735
667
  const release = () => { if (active) { active = false; dispose() } }
736
668
  owned.push(release); keepDisposer(ctx, release, label)
669
+ return release
737
670
  }
738
- const on = options.on ?? ((...args) => ctx.on(...args))
739
- // 官方插值两层共享一份变量注册:运行时事实按 assembly 求值,非法名走别名改写。
740
- const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce, keep)
741
- const conditional = configs.filter(config => typeof config.rule?.when === 'function' && ['system-section', 'runtime-context'].includes(config.layer))
742
- const regular = configs.filter(config => !conditional.includes(config))
743
- const registered = [
744
- wireSystemSections(ctx, regular.filter((config) => config.layer === 'system-section'), registry, warnOnce, keep),
745
- wireRuntimeContexts(ctx, regular.filter((config) => config.layer === 'runtime-context'), registry, warnOnce, keep, on),
746
- wireRuleTextContributions(ctx, conditional, registry, warnOnce, keep, on),
747
- wireAgentRequests(ctx, configs.filter((config) => config.layer === 'agent-request'), warnOnce, on),
748
- wireLlmStreams(ctx, configs.filter((config) => config.layer === 'llm-stream'), warnOnce, on),
749
- wireToolPipelines(ctx, configs.filter((config) => config.layer === 'tool-pipeline'), warnOnce, on),
750
- wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce, on),
751
- wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce, on),
752
- owned,
753
- ].flat().filter((disposer) => typeof disposer === 'function')
754
- return () => {
755
- for (const dispose of registered.splice(0)) {
756
- try {
757
- dispose()
758
- } catch {
759
- // 释放失败不得反过来打断卸载流程。
760
- }
671
+ const on = (...args) => keep((options.on ?? ((...values) => ctx.on(...values)))(...args), `${name}: ${args[0]}`)
672
+ const dispose = () => {
673
+ for (const release of owned.splice(0).reverse()) { try { release() } catch {} }
674
+ }
675
+ try {
676
+ // 注册即纳入 owned,后续注册失败也能释放已取得的变量和监听器。
677
+ /** 实际注册名与序号:重排器只认这里,不从 configs 反推。 */
678
+ const ordered = { sections: [], contexts: [] }
679
+ const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce, keep)
680
+ const conditional = configs.filter(config => ['system-section', 'runtime-context'].includes(config.layer)
681
+ && (typeof config.rule?.when === 'function' || typeof config.actionWhen === 'function'
682
+ || (config.rule !== undefined || config.params?.templateEvaluation === true) && config.params?.complete !== true && config.params?.suppressRuntimeContext !== true))
683
+ const regular = configs.filter(config => !conditional.includes(config))
684
+ wireSystemSections(ctx, regular.filter((config) => config.layer === 'system-section'), registry, warnOnce, keep, ordered)
685
+ wireRuntimeContexts(ctx, regular.filter((config) => config.layer === 'runtime-context'), registry, warnOnce, keep, on, ordered)
686
+ wireRuleTextContributions(ctx, conditional, registry, warnOnce, keep, on, ordered)
687
+ wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce, on, options.turnStopBudgets)
688
+ wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce, on)
689
+ // 官方 order 只提供初始槽位;configOrder 覆盖段/上下文的先后(平台段与其他插件段不动)。
690
+ for (const collection of ['sections', 'contexts']) {
691
+ registerDeliveryOrder(ctx, keep, collection, orderedNamesOf(ordered[collection]))
761
692
  }
693
+ return dispose
694
+ } catch (error) {
695
+ dispose()
696
+ throw error
762
697
  }
763
698
  }