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
@@ -4,8 +4,9 @@
4
4
  * 任一提示词配置失败只跳过该提示词配置并 warnOnce,绝不让注入 bug 卡死会话。
5
5
  *
6
6
  * 装配路径有两条,批执行算法只有一份(runPreStepBatch):
7
- * - 管理路径:宿主 prompt-tool 插件提供 `promptToolPreStep` 协调服务时,本来源
8
- * (预设 mount)把配置注册给协调器,由协调器统一执行预设来源与独立指令文件来源;
7
+ * - 管理路径:宿主 prompt-tool 插件提供 pre-step 协调服务(服务名见 `service-names.mjs`)时,本来源
8
+ * (预设 mount)把配置注册给协调器,由协调器统一执行各预设来源;官方指令正文的
9
+ * 逐文件过滤归协调器的 prepend 监听器,旧生成卡的 `agents-file-*` 按 id 前缀跳过;
9
10
  * - 独立路径:没有协调服务(引擎被复制到无宿主的目录复用)时,本行 ctx 注册本地
10
11
  * pre-step 监听器,只执行自身预设配置。
11
12
  * 协调服务迟到或消失时按「先撤旧再启新」切换,任一时刻每 scope 只有一条执行路径。
@@ -20,21 +21,28 @@ import {
20
21
  keepDisposer,
21
22
  matchesModel,
22
23
  newMessageId,
23
- sessionEvents,
24
24
  } from './shared.mjs'
25
+ // 去重判据读**当前模型可见上下文**(视图①),不读完整历史:被压缩遮蔽的注入要重新注入。
26
+ import { eventMessage, moduleIdOf, moduleOf, pluginIdentityOf, identityOf, identityStampOf, identityHit, injectedMessage, actionStamp, findActionDelivery } from './delivery.mjs'
25
27
  import { interpolateVariables } from './interpolate.mjs'
26
- import { conditionHit, userMessagesText } from './condition.mjs'
27
28
  import { createEpochPromotion } from './compaction-epoch.mjs'
28
29
  import { wireLayers } from './layers.mjs'
29
- import { sessionVarsSnapshot } from './session-vars.mjs'
30
- import { selectStWorldBook } from './st-world-book.mjs'
30
+ import { renderTemplate } from './template-frame.mjs'
31
+ import { selectContextActivation } from './context-activation.mjs'
31
32
  import { compareConfigSequence } from './order.mjs'
32
- import { ruleFrame, ruleMatches, actionMatches } from './conditions/evaluation.mjs'
33
+ import { ruleFrame, actionMatches } from './conditions/evaluation.mjs'
34
+ import { composePreStepSources, promotionOwnerOf } from './pre-step-source.mjs'
35
+ // 标记键名与「已登记进 config 白名单」同源(schema.mjs),此处不另写字面量。
36
+ import { SESSION_VARIABLES_DISABLED } from './schema.mjs'
37
+ import { PRE_STEP_COORDINATOR_SERVICE } from './service-names.mjs'
33
38
 
34
39
  const name = 'prompt-config-engine'
35
40
 
36
- /** 已确认投递身份的进程内快路径上限;真相在持久事件流。 */
37
- // ponytail: 身份条数上限防无界增长(Map 按投递身份累积);若需精确淘汰改 LRU。
41
+ /**
42
+ * 「已确认投递」快路径的条目上限。真相是**当前上下文扫描**(见 injectedMessage):
43
+ * 这份 memo 只省一次扫描,丢了只是多扫一次,清空不改变任何判定结果,也不再需要淘汰策略。
44
+ */
45
+ // shortcut: 上限防无界增长(Map 按「身份 × 代次」累积);若需精确淘汰改 LRU。
38
46
  const MAX_MEMO_CONFIGS = 4096
39
47
 
40
48
  /**
@@ -42,64 +50,52 @@ const MAX_MEMO_CONFIGS = 4096
42
50
  * 缺失/形状不符时返回 undefined:本行回退独立执行路径。
43
51
  */
44
52
  function coordinatorOf(ctx) {
45
- const service = getService(ctx, 'promptToolPreStep')
53
+ const service = getService(ctx, PRE_STEP_COORDINATOR_SERVICE)
46
54
  return service !== null && typeof service === 'object' && typeof service.registerPreset === 'function'
47
55
  ? service
48
56
  : undefined
49
57
  }
50
58
 
51
- /** 事件数据兼容两层形状:event.data 本身是消息,或 event.data.message 是消息。 */
52
- function eventMessage(event) {
53
- const data = event?.data
54
- if (data === null || typeof data !== 'object') return undefined
55
- const message = data.message !== null && typeof data.message === 'object' ? data.message : data
56
- return message?.source !== null && typeof message?.source === 'object' ? message : undefined
57
- }
58
-
59
- /** 合并身份:merged 模式按位置分组(merged:<position>);separate 模式保持自身身份。 */
60
- function mergedIdentity(config) {
61
- if (config.mergeMode !== 'merged') return config.id
62
- return `merged:${config.position}`
63
- }
64
-
65
- /** 去重身份:merged 组用位置命名空间,独立配置用自身身份。 */
66
- function identityOf(config) {
67
- return config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
59
+ /** 当前消息批内是否已有该提示词配置注入(每轮去重)。 */
60
+ function hasInBatch(config, messages) {
61
+ const value = identityOf(config)
62
+ return messages.some((message) => identityHit(message?.source, config, value))
68
63
  }
69
64
 
70
65
  /**
71
- * 消息来源的插件身份(去重身份)。v4 起 `source.kind` 是生产者名(`plugin:<name>`),
72
- * `source.plugin` 只作为旧日志字段保留——两条通道都要认,否则去重记账会在格式换代后静默失效。
66
+ * 快路径条目键:通道 + 模块维 + 有效期戳 + 值。分隔符用 NUL(身份值里可能带 `:`)。
67
+ *
68
+ * 位置替换与内容投影的代次共同组成戳;任一变化都重新查询当前可见投递。
69
+ * 无 surface(降级到完整历史)时戳恒为 `-`:完整历史不会丢事件,条目一直有效。
73
70
  */
74
- function pluginIdentityOf(source) {
75
- if (source === null || typeof source !== 'object') return undefined
76
- if (typeof source.plugin === 'string' && source.plugin.length > 0) return source.plugin
77
- return typeof source.kind === 'string' && source.kind.startsWith('plugin:')
78
- ? source.kind.slice('plugin:'.length)
79
- : undefined
71
+ function memoKey(field, moduleId, value, stamp) {
72
+ // 戳不可用(null)时调用方不查也不记。
73
+ return `${field}\u0000${moduleId ?? ''}\u0000${stamp}\u0000${value}`
80
74
  }
81
75
 
82
- function hasInjected(config, session) {
83
- const value = identityOf(config)
84
- return sessionEvents(session).some((event) => {
85
- const message = eventMessage(event)
86
- // 双通道去重:kind(外来/第三方消息,如 context-gate 的 instruction-hint)或
87
- // plugin(本引擎注入的命名空间,merged 组用 merged:<position>)。
88
- return message?.source?.kind === config.sourceKind || pluginIdentityOf(message?.source) === value
89
- })
90
- }
91
-
92
- /** 当前消息批内是否已有该提示词配置注入(每轮去重)。 */
93
- function hasInBatch(config, messages) {
94
- const value = identityOf(config)
95
- return messages.some((message) =>
96
- message?.source?.kind === config.sourceKind || pluginIdentityOf(message?.source) === value)
76
+ /**
77
+ * 本会话当前可见上下文的代次戳,也是 memo 键的失效维。
78
+ *
79
+ * - 代次对:surface 的 `replaceGeneration:contentGeneration`;
80
+ * - `'-'`:无 surface(降级到完整历史),事件不会消失,条目一直有效;
81
+ * - `null`:**有 `surface.nodes` 却没有数字代次** —— memo 此时不可用:代次是它唯一的失效
82
+ * 信号,而真相(`currentEvents`)仍跟着 `nodes` 走,快路径会把被压缩移走的注入永久记成
83
+ * 「已投递」。这种宿主形态是真实情形(恢复、重放、其它宿主、既有桩;见
84
+ * `compaction-epoch.mjs` 的失效条件③),调用方据此跳过 memo、退回当前上下文扫描
85
+ * —— memo 只是省一次扫描的缓存,跳过不改变任何判定结果。
86
+ */
87
+ function contextStamp(session) {
88
+ const generation = session?.surface?.replaceGeneration
89
+ if (typeof generation === 'number') return `${generation}:${session.surface.contentGeneration ?? 0}`
90
+ return Array.isArray(session?.surface?.nodes) ? null : '-'
97
91
  }
98
92
 
99
93
  /**
100
- * 按投递身份取「已确认投递」的会话集合。记账只发生在宿主真正接纳并持久化该消息之后
101
- * (见 confirmDelivered):候选在瀑布外层被门控/策略剥离时不算已投递,否则晋升后
102
- * (门控放行时)正文会永久缺失。集合上限按会话数截断,真相仍在持久事件流。
94
+ * 按投递身份取「已确认投递」的会话集合 —— 纯性能缓存(省一次当前上下文扫描),
95
+ * **不是**真相:条目可随时丢弃(上限、`release()`、`clear()`),丢了只多扫一次。
96
+ *
97
+ * 记账只发生在宿主真正接纳并持久化该消息之后(见 confirmDelivered):候选在瀑布外层被
98
+ * 门控/策略剥离时不算已投递,否则晋升后(门控放行时)正文会永久缺失。
103
99
  */
104
100
  function deliveredSessions(memo, key) {
105
101
  let set = memo.get(key)
@@ -114,30 +110,49 @@ function deliveredSessions(memo, key) {
114
110
 
115
111
  /**
116
112
  * 记录一次宿主已接纳的注入消息(由调用方在 session/event 里转发)。
117
- * plugin(本引擎身份,merged 组用 merged:<position>)与 kind(外来通道,如
118
- * context-gate 的 instruction-hint)都以会话为界记账,与 hasInjected 的双通道语义一致。
113
+ * plugin(本引擎身份,merged 组用 merged:<position>)、kind(外来通道,如
114
+ * context-gate 的 instruction-hint)与消息的模块维都以会话为界记账,
115
+ * 与 identityHit 的三元组判据一致。
119
116
  */
120
117
  export function confirmDelivered(memo, session, event) {
121
118
  if (memo === null || memo === undefined || session === null || session === undefined) return
122
119
  const source = eventMessage(event)?.source
123
120
  if (source === null || typeof source !== 'object') return
121
+ const moduleId = moduleOf(source)
122
+ const stamp = contextStamp(session)
123
+ // 戳不可用 → 不记账:这条快路径无法在视图变化时失效,判定改由当前上下文扫描给出。
124
+ if (stamp === null) return
124
125
  for (const field of ['plugin', 'kind']) {
125
126
  const value = source[field]
126
127
  if (typeof value !== 'string' || value.length === 0) continue
127
- deliveredSessions(memo, `${field}:${value}`).add(session.id)
128
+ deliveredSessions(memo, memoKey(field, moduleId, value, stamp)).add(session.id)
128
129
  }
129
130
  // v4 起生产者身份挂在 kind(`plugin:<name>`),旧日志才有 plugin 字段;
130
131
  // 两种形态都补记到 plugin: 账本,alreadyDelivered 的 `confirmed('plugin', …)` 才不会漏。
131
132
  const plugin = pluginIdentityOf(source)
132
- if (plugin !== undefined && source.plugin === undefined) deliveredSessions(memo, `plugin:${plugin}`).add(session.id)
133
+ if (plugin !== undefined && source.plugin === undefined) {
134
+ deliveredSessions(memo, memoKey('plugin', moduleId, plugin, stamp)).add(session.id)
135
+ }
133
136
  }
134
137
 
135
- /** dedupe=session:本会话是否已有该身份的已确认投递(快路径 + 持久事件真相)。 */
138
+ /** dedupe=session:本会话**当前上下文**里是否已有该身份的已确认投递(快路径 + 当前上下文真相)。 */
136
139
  function alreadyDelivered(config, session, memo) {
137
- const confirmed = (field, value) => memo.get(`${field}:${value}`)?.has(session.id) === true
140
+ if (config.linkedDelivery) return config.mergeMode === 'merged'
141
+ ? injectedMessage(config, session) !== undefined : findActionDelivery(session, config) !== undefined
142
+ const stamp = contextStamp(session)
143
+ // 模块维有两种命中:本模块的键,以及升级前注入(无模块维)那份的保守键。
144
+ const modules = [moduleIdOf(config, 'sourceModuleId') ?? '', '']
145
+ // 戳不可用(有 surface 却无代次)→ 不查 memo:那里记的「已投递」可能已被视图改写。
146
+ const confirmed = (field, value) => stamp !== null && modules.some((moduleId) =>
147
+ memo.get(memoKey(field, moduleId, value, stamp))?.has(session.id) === true)
138
148
  if (confirmed('plugin', identityOf(config))) return true
139
149
  if (typeof config.sourceKind === 'string' && confirmed('kind', config.sourceKind)) return true
140
- return hasInjected(config, session)
150
+ const message = injectedMessage(config, session)
151
+ if (message === undefined) return false
152
+ // 慢路径命中即按既有入口回填快路径(真相仍是当前上下文,不直接写 memo):
153
+ // 无 id 的会话不记账——memo 的键就是 session.id,记了会让两个无 id 会话串味。
154
+ if (session.id !== undefined) confirmDelivered(memo, session, { data: { message } })
155
+ return true
141
156
  }
142
157
 
143
158
  /**
@@ -148,7 +163,7 @@ const PRE_STEP_ROLE = 'user'
148
163
 
149
164
  /**
150
165
  * 记录一次角色降级:保留原角色、明确告警、不改正文,也不把非法值传给宿主。
151
- * 同一配置只告警一次(warnOnce),且不记录正文或用户内容。
166
+ * 同一句告警只记一次(warnOnce 按消息去重),且不记录正文或用户内容。
152
167
  */
153
168
  function downgradeRole(config, requested, warnOnce) {
154
169
  if (typeof requested !== 'string' || requested === PRE_STEP_ROLE) return undefined
@@ -157,21 +172,33 @@ function downgradeRole(config, requested, warnOnce) {
157
172
  }
158
173
 
159
174
  /** 构造默认 user 消息;策略返回完整 patch 时覆盖对应字段,但非法角色在出口兜底降级。 */
160
- function buildMessage(config, resolved, warnOnce) {
175
+ function buildMessage(config, resolved, warnOnce, session) {
161
176
  const text = typeof resolved.text === 'string' ? resolved.text : ''
162
177
  const defaultContent = config.texts.length > 0
163
178
  ? config.texts.map((item) => ({ type: 'text', text: item }))
164
179
  : [{ type: 'text', text }]
165
- const sourceValue = mergedIdentity(config)
180
+ // 盖章身份与 alreadyDelivered 的查找键同源(identityOf):显式 identity 才真正共享去重,
181
+ // 也不会与他卡的 id 误撞;merged 组 identityOf 本就等于 mergedIdentity,存量输出不变。
182
+ const sourceValue = identityOf(config)
166
183
  // 策略 patch(templateFile 的 role 等)与配置声明都必须经过同一出口判定。
167
184
  const requested = downgradeRole(config, typeof resolved.role === 'string' ? resolved.role : config.role, warnOnce)
168
- const base = resolved.source !== null && typeof resolved.source === 'object'
169
- ? resolved.source
185
+ // 模块维与 `identityStampOf` 同源(`moduleIdOf`:空串与缺失同等):两条分支必须盖同一份章,
186
+ // 否则解析器自带 source 的消息比合成分支多带一个空模块维,memo 与判定就会分叉。
187
+ const moduleId = moduleIdOf(config, 'sourceModuleId')
188
+ const source = resolved.source !== null && typeof resolved.source === 'object'
189
+ // 解析器自带的 source 也要盖章:否则这类消息(如 fill=instruction-hint 的
190
+ // `{ kind: 'instruction-hint' }`)只带 kind 通道,dedupe=session 每步重复注入。
191
+ // 解析器自带的 kind 优先(缺省 kind 只在它缺席时补),plugin 只在其非空时覆盖身份。
192
+ ? {
193
+ ...resolved.source,
194
+ plugin: typeof resolved.source.plugin === 'string' && resolved.source.plugin.length > 0 ? resolved.source.plugin : sourceValue,
195
+ ...(resolved.source.kind === undefined ? { kind: identityStampOf({ ...config, sourceKind: undefined }).kind } : {}),
196
+ ...(moduleId === undefined ? {} : { moduleId }),
197
+ }
170
198
  : {
171
- // v4 要求 kind 是**生产者名**:声明了 sourceKind 就用它,否则用与 pluginMessage 同一形状的
172
- // `plugin:<身份>`。裸 kind(undefined / 'plugin')都会被 codec 拒绝,历史走的是同一个坑。
173
- kind: typeof config.sourceKind === 'string' && config.sourceKind.length > 0 ? config.sourceKind : `plugin:${sourceValue}`,
174
- plugin: sourceValue,
199
+ // 模块维是身份判据的新一维,但**不并进身份字符串**:`source.plugin` 的格式是
200
+ // blockPlugins 的精确匹配契约,一字不改;整模块复制的区分只靠这个独立字段。
201
+ ...identityStampOf(config),
175
202
  ...(typeof config.form === 'string' ? { form: config.form } : {}),
176
203
  ...(typeof config.summary === 'string' && config.summary.length > 0 ? { summary: config.summary } : {}),
177
204
  }
@@ -179,7 +206,41 @@ function buildMessage(config, resolved, warnOnce) {
179
206
  id: typeof resolved.id === 'string' && resolved.id.length > 0 ? resolved.id : newMessageId(config.id),
180
207
  role: PRE_STEP_ROLE,
181
208
  content: Array.isArray(resolved.content) ? resolved.content : defaultContent,
182
- source: requested === undefined ? base : { ...base, requestedRole: requested },
209
+ source: { ...source, ...actionStamp(config, session), ...(requested === undefined ? {} : { requestedRole: requested }) },
210
+ }
211
+ }
212
+
213
+ /**
214
+ * 本步(一个 pre-step 批次,含被 `ruleActions` 切开的多次 flush)的批级快照:
215
+ * 合格配置集合、模板位置身份集合与 ST 世界书选择,全部在批首取定一次。
216
+ *
217
+ * 三者必须同一集合:`qualified` 每配置只判一次(R02),ST 世界书的组互斥与概率/
218
+ * 粘滞窗口按**步**成立——旧实现每个 flush 各选一次,同组两条会各赢一条(F06)。
219
+ * 返回 undefined = 本批无法执行(reject / 无 agent / 无 session),调用方原样返回。
220
+ */
221
+ function batchScope(options, frame) {
222
+ const { agent, decision, configs, promotion, warnOnce } = options
223
+ const session = agent?.session
224
+ if (decision === null || typeof decision !== 'object' || decision.kind === 'reject'
225
+ || agent === undefined || session === undefined) return undefined
226
+ const messages = Array.isArray(decision.messages) ? [...decision.messages] : []
227
+ const { main, withSubagents } = promotion
228
+ const delegated = isDelegated(session)
229
+ // 本批资格判定(唯一实现):层通道、受众、模型、晋升与 live 规则条件。
230
+ // 条件判定放在去重之前:未命中的配置不算「已注入」,条件恢复后仍应能注入。
231
+ // ST 模板的跨配置变量帧按 order 预求值,但只有这里的获准集合才允许产生副作用
232
+ // (未命中的 setter 提前 setvar 会污染同批 reader)——渲染器不再复制判定。
233
+ const qualified = configs.filter((config) => config.layer === 'pre-step'
234
+ && !(config.audience === 'main' && delegated)
235
+ && !(config.audience === 'subagent' && !delegated)
236
+ && matchesModel(config.modelScope, agent.options?.model)
237
+ && (config.promotion !== 'main' || main.status(agent).promoted)
238
+ && (config.promotion !== 'include-subagents' || withSubagents.status(agent).promoted)
239
+ && actionMatches(config, frame))
240
+ return {
241
+ qualified: new Set(qualified),
242
+ contextActivation: selectContextActivation(qualified, session, messages, warnOnce, config => config[SESSION_VARIABLES_DISABLED] === true
243
+ ? { module: {}, global: {} } : config.variableStore?.read(config.sourceModuleId) ?? { module: config.variables, global: {} }),
183
244
  }
184
245
  }
185
246
 
@@ -193,65 +254,74 @@ function buildMessage(config, resolved, warnOnce) {
193
254
  *
194
255
  * @param options.configs 已过滤 enabled 与互斥组的最终提示词配置列表。
195
256
  * @param options.promotion { main, withSubagents } 晋升追踪器(真相在持久事件流)。
196
- * @param options.memo 已确认投递身份的进程内 session 去重快路径(见 confirmDelivered)。
257
+ * @param options.memo 已确认投递身份的进程内 session 去重快路径(纯缓存,丢了只多一次
258
+ * 当前上下文扫描;见 confirmDelivered)。
197
259
  * @returns 注入后的 decision;reject、缺 agent/session、全部跳过或异常时原样返回。
198
260
  */
199
261
  export async function runPreStepBatch(options) {
200
- const initialFrame = options.ruleFrame ?? ruleFrame('agent/pre-step', [{ agent: options.agent, messages: options.decision?.messages ?? [] }], options.warnOnce, options.ctx)
201
- if (!options.ruleActions?.length) return runPromptConfigBatch({ ...options, ruleFrame: initialFrame })
202
- const entries = [...options.configs.map(config => ({ ...config, config })), ...options.ruleActions].sort(compareConfigSequence)
262
+ if (!Array.isArray(options.sources)) {
263
+ throw new TypeError('runPreStepBatch requires a sources array')
264
+ }
265
+ const sources = options.sources
266
+ const composed = composePreStepSources(sources)
267
+ if (composed.configs.length === 0 && composed.ruleActions.length === 0) return options.decision
268
+ const ctx = options.ctx ?? sources[0]?.ctx
269
+ const initialFrame = options.ruleFrame ?? ruleFrame('agent/pre-step', [{ agent: options.agent, messages: options.decision?.messages ?? [] }], options.warnOnce, ctx, composed.report)
270
+ // 批级快照与单配置失败同款 fail-safe:资格判定或 ST 世界书选择抛错时只告警一次并跳过本步
271
+ // 注入(scope 留空 → runPromptConfigBatch 原样返回 decision),绝不把异常抛进
272
+ // agent/pre-step waterfall。世界书键的 `interpolateVariables` 在 selectStWorldBook 的
273
+ // per-config try 之外,配置错误不得卡死会话。
274
+ const batchOptions = { ...options, ...composed, ctx }
275
+ let scope
276
+ try {
277
+ scope = batchScope(batchOptions, initialFrame)
278
+ } catch (error) {
279
+ options.warnOnce?.(`${name}: prompt config failed, skipping: ${String((error && error.message) || error)}`)
280
+ }
281
+ // 同一批的配置与动作按配置序号统一排序;没有规则动作时也必须排,不能依赖来源注册顺序。
282
+ const orderedConfigs = [...composed.configs].sort(compareConfigSequence)
283
+ if (composed.ruleActions.length === 0) {
284
+ return runPromptConfigBatch({ ...batchOptions, configs: orderedConfigs, ruleFrame: initialFrame, scope })
285
+ }
286
+ const entries = [...orderedConfigs.map(config => ({ ...config, config })), ...composed.ruleActions].sort(compareConfigSequence)
203
287
  let decision = options.decision
204
288
  if (decision?.kind === 'reject') return decision
205
289
  let pending = []
206
290
  const placement = { beforeAll: new Set(), afterUser: new Set() }
207
291
  const flush = async () => {
208
- if (pending.length) decision = await runPromptConfigBatch({ ...options, decision, configs: pending, ruleFrame: initialFrame, placement })
292
+ if (pending.length) decision = await runPromptConfigBatch({ ...batchOptions, decision, configs: pending, ruleFrame: initialFrame, placement, scope })
209
293
  pending = []
210
294
  }
211
295
  for (const entry of entries) {
212
296
  if (entry.config) { pending.push(entry.config); continue }
213
297
  await flush()
214
- if (ruleMatches(entry.rule, initialFrame)) decision = await entry.handler(options.payload ?? initialFrame.args[0], () => decision, initialFrame)
298
+ if (actionMatches(entry, initialFrame)) decision = await entry.handler(options.payload ?? initialFrame.args[0], () => decision, initialFrame)
215
299
  }
216
300
  await flush()
217
301
  return decision
218
302
  }
219
303
 
220
304
  async function runPromptConfigBatch(options) {
221
- const { ctx, agent, decision, configs, promotion, memo, warnOnce } = options
305
+ const { ctx, agent, decision, configs, memo, warnOnce, scope } = options
222
306
  if (decision === null || typeof decision !== 'object') return decision
223
307
  if (decision.kind === 'reject') return decision
224
308
  if (agent === undefined) return decision
225
309
  const session = agent.session
226
310
  if (session === undefined) return decision
227
- const { main, withSubagents } = promotion
311
+ // 批级快照由 runPreStepBatch 在批首取定;这里只消费,不再重新判定。
312
+ if (scope === undefined) return decision
228
313
  try {
229
314
  const messages = Array.isArray(decision.messages) ? [...decision.messages] : []
230
- // 条件判定的匹配对象在本批进入时取定:批次内后续注入不改变本批的判定依据。
231
- const userText = userMessagesText(messages)
232
315
  let changed = false
233
316
 
234
317
  const due = []
235
- // 本批资格判定(唯一实现):层通道、受众、模型、晋升与声明式条件。
236
- // 条件判定放在去重之前:未命中的配置不算「已注入」,条件恢复后仍应能注入。
237
- // ST 模板的跨配置变量帧按 order 预求值,但只有这里的获准集合才允许产生副作用
238
- // (未命中的 setter 提前 setvar 会污染同批 reader)——渲染器不再复制判定。
239
- const delegated = isDelegated(session)
240
- const qualified = (config) => config.layer === 'pre-step'
241
- && !(config.audience === 'main' && delegated)
242
- && !(config.audience === 'subagent' && !delegated)
243
- && matchesModel(config.modelScope, agent.options?.model)
244
- && (config.promotion !== 'main' || main.status(agent).promoted)
245
- && (config.promotion !== 'include-subagents' || withSubagents.status(agent).promoted)
246
- && conditionHit(config, { userText })
247
- && actionMatches(config, options.ruleFrame)
248
- const qualifiedConfigs = configs.filter(qualified)
249
- // 协调器为绑定来源 ctx 会复制 config;renderSt 函数身份在副本间保持不变。
250
- const eligible = new Set(qualifiedConfigs.map(config => config.renderSt))
251
- const stWorldBook = selectStWorldBook(qualifiedConfigs, session, messages, warnOnce)
252
318
  for (const config of configs) {
253
319
  try {
254
- if (!qualified(config)) continue
320
+ if (!scope.qualified.has(config)) continue
321
+ if (config.position === 'after-user' && !messages.some(message => message?.source?.kind === 'user')) {
322
+ warnOnce(`${name}: after-user config(s) ${config.id} skipped this batch — no user message anchor`)
323
+ continue
324
+ }
255
325
 
256
326
  if (config.dedupe === 'session') {
257
327
  if (alreadyDelivered(config, session, memo)) continue
@@ -259,31 +329,33 @@ async function runPromptConfigBatch(options) {
259
329
  continue
260
330
  }
261
331
 
262
- const resolved = await config.resolve({ ctx, agent, session, decision, messages, stWorldBookSelected: stWorldBook.has(config) })
332
+ const signal = options.payload?.signal
333
+ if (signal?.aborted) continue
334
+ const resolved = await config.resolve({ ctx, agent, session, signal, decision, messages, contextActivationSelected: scope.contextActivation.has(config) })
335
+ if (signal?.aborted) continue
263
336
  if (resolved === null || resolved === undefined) continue
264
337
  const patched = { ...resolved }
265
- // params 并入插值变量:ST 变量(setvar/getvar 收集 + 预设参数)顶层 key 直接可插值
266
- //(含中文 key 如 {{接受值}}——引擎正则已支持 Unicode 字母)。
267
- // 会话变量(session_var 工具维护)覆盖配置/预设默认:resolved > 会话 > params > 配置。
338
+ // 按来源读取最新持久值;无变量正文不因另一个坏候选的读盘失败撤回。
268
339
  const mergedVars = {
269
- ...config.variables,
270
- ...config.params,
271
- ...sessionVarsSnapshot(session),
340
+ ...(config[SESSION_VARIABLES_DISABLED] === true ? {} : config.params?.templateEvaluation !== true && [patched.text, ...config.texts].some(text => typeof text === 'string' && text.includes('{{'))
341
+ ? config.variableStore?.read(config.sourceModuleId).effective ?? config.variables : config.variables),
272
342
  ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}),
273
343
  }
274
- if (typeof config.renderSt === 'function') {
275
- patched.text = config.renderSt(agent, messages, warnOnce, options, eligible)
344
+ if (config.params?.templateEvaluation === true) {
345
+ patched.text = renderTemplate({ ...config, texts: config.texts.length ? config.texts : [patched.text ?? ''] }, { agent, signal }, { enabled: config[SESSION_VARIABLES_DISABLED] !== true, warn: warnOnce })
276
346
  patched.content = patched.text.length > 0 ? [{ type: 'text', text: patched.text }] : []
277
347
  } else if (typeof patched.text === 'string') {
278
348
  // 提示词配置级模板变量 + filler 变量 + 内置环境变量插值。
279
- patched.text = interpolateVariables(patched.text, mergedVars, session)
349
+ // 第 5 参是模板位置身份(本配置的 id):{{pick}} 的 seed 只认「同会话 + 同模板位置 + 同一次出现」,
350
+ // 缺省空串会让不同配置的首个 pick 退化成同一个 seed。
351
+ patched.text = interpolateVariables(patched.text, mergedVars, session, undefined, config.id)
280
352
  }
281
- if (config.texts.length > 0 && typeof config.renderSt !== 'function') {
353
+ if (config.texts.length > 0 && config.params?.templateEvaluation !== true) {
282
354
  const blocks = config.texts
283
- .map((item) => interpolateVariables(item, mergedVars, session))
355
+ .map((item, index) => interpolateVariables(item, mergedVars, session, undefined, `${config.id}#${index}`))
284
356
  .filter((item) => item.length > 0)
285
357
  .map((item) => ({ type: 'text', text: item }))
286
- if (blocks.length > 0) patched.content = blocks
358
+ patched.content = blocks
287
359
  }
288
360
  const hasText = typeof patched.text === 'string' && patched.text.length > 0
289
361
  const hasContent = Array.isArray(patched.content) && patched.content.length > 0
@@ -317,7 +389,7 @@ async function runPromptConfigBatch(options) {
317
389
  const planned = []
318
390
  for (const group of orderedGroups) {
319
391
  const base = group[0]
320
- const message = buildMessage(base.config, base.resolved, warnOnce)
392
+ const message = buildMessage(base.config, base.resolved, warnOnce, agent.session)
321
393
  if (group.length > 1) {
322
394
  message.content = group.flatMap((entry) => {
323
395
  if (Array.isArray(entry.resolved.content)) return entry.resolved.content
@@ -326,7 +398,7 @@ async function runPromptConfigBatch(options) {
326
398
  : []
327
399
  })
328
400
  if (message.source !== null && typeof message.source === 'object') {
329
- message.source = { ...message.source, plugin: mergedIdentity(base.config) }
401
+ message.source = { ...message.source, plugin: identityOf(base.config) }
330
402
  }
331
403
  // 合并组的角色由首条配置决定;其余成员声明的非法角色同样要告警并留下降级事实。
332
404
  for (const entry of group.slice(1)) {
@@ -345,7 +417,7 @@ async function runPromptConfigBatch(options) {
345
417
  // 同位置批量插入:planned 已按 order 升序,多元素 splice/unshift/push 保持该顺序。
346
418
  // 去重记账不在这里:候选被外层门控剥离时也必须保持「未投递」,由 confirmDelivered 确认。
347
419
  const markGroup = (group) => {
348
- for (const entry of group) stWorldBook.commit?.(entry.config)
420
+ for (const entry of group) scope.contextActivation.commit?.(entry.config)
349
421
  }
350
422
  const beforeAll = planned.filter((item) => item.position === 'before-all')
351
423
  const afterUser = planned.filter((item) => item.position !== 'before-all' && item.position !== 'after-all')
@@ -359,6 +431,12 @@ async function runPromptConfigBatch(options) {
359
431
  changed = true
360
432
  }
361
433
  const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
434
+ if (afterUser.length > 0 && userIndex < 0) {
435
+ // 本批没有真实用户消息可锚定:本步跳过(不记账,next 含 user 的批仍会注入)。不兜底到
436
+ // before-all / after-all —— 「锚点是真实用户消息、不挂到插件消息之后」是插入点契约。
437
+ // 按消息去重后,不同配置组合各记一次,不再互相占名额。
438
+ warnOnce(`${name}: after-user config(s) ${afterUser.map((item) => item.group[0].config.id).join(', ')} skipped this batch — no user message anchor`)
439
+ }
362
440
  if (afterUser.length > 0 && userIndex >= 0) {
363
441
  const last = options.placement === undefined ? -1 : messages.findLastIndex(message => options.placement.afterUser.has(message))
364
442
  messages.splice(Math.max(userIndex, last) + 1, 0, ...afterUser.map((item) => item.message))
@@ -379,14 +457,6 @@ async function runPromptConfigBatch(options) {
379
457
  }
380
458
  }
381
459
 
382
- /**
383
- * 把一组运行时提示词配置装配为注入执行器(独立模式 / 协调器管理模式的接线口)。
384
- *
385
- * @param options.prepend 是否以 prepend 注册本地 pre-step(合并行恒 true;由参数决定)。
386
- * @param options.sourceId 协调器里的来源 id(默认取首条配置 id 派生)。
387
- * @param options.officialInstructions 本 mount 的组合是否仍挂着官方指令加载行
388
- * (负责人冲突事实;协调器据此拒绝同时注入文件正文)。
389
- */
390
460
  function effectivePromptConfigs(configs) {
391
461
  const list = configs.filter((config) => config !== undefined && config !== null)
392
462
  const claimedGroups = new Set()
@@ -400,120 +470,135 @@ function effectivePromptConfigs(configs) {
400
470
  })
401
471
  }
402
472
 
403
- /** 多模块仍各自拥有来源/互斥组;只有非 pre-step 层汇总接线,避免模块块状执行。 */
404
- export function applyPromptConfigSources(ctx, sources, options = {}) {
473
+ /**
474
+ * 唯一的 pre-step 装配入口:来源自带 ctx/onOutcome/promotion,本函数只做固定效果接线、
475
+ * 协调器注册与(无协调器时的)单 listener 批执行。
476
+ *
477
+ * 所有来源必须共享同一个 ctx;没有 promotion 时由协调器或本次调用补一个 fallback owner,
478
+ * 已有 owner 不一致则 fail loud。
479
+ */
480
+ export function applyPromptConfigSources(sources, options = {}) {
481
+ if (!Array.isArray(sources) || sources.length === 0) return () => {}
482
+ const ctx = sources[0].ctx
483
+ if (sources.some(source => source.ctx !== ctx)) {
484
+ throw new TypeError('prompt-tool: all pre-step sources in one apply call must share the same ctx')
485
+ }
486
+ const warnOnce = createWarnOnce(ctx, name)
405
487
  const selected = sources.map(source => ({ ...source, configs: effectivePromptConfigs(source.configs) }))
406
- const releases = selected.map(source => applyPromptConfigs(ctx, source.configs, { ...options, ...source, layers: false }))
407
- const releaseLayers = options.layers === false ? () => {} : wireLayers(ctx, selected.flatMap(source => source.configs)
408
- .filter(config => config.layer !== 'pre-step'), createWarnOnce(ctx, name))
409
- return () => {
410
- for (const release of releases) release()
411
- releaseLayers()
488
+ const owner = promotionOwnerOf(selected)
489
+ const coordinator = coordinatorOf(ctx)
490
+ const coordinatorPromotion = coordinator?.promotion
491
+ if (coordinator !== undefined && coordinatorPromotion === undefined) {
492
+ throw new TypeError('prompt-tool: pre-step coordinator v2 must expose a promotion owner')
412
493
  }
413
- }
414
-
415
- export function applyPromptConfigs(ctx, configs, options = {}) {
416
- const list = configs.filter((config) => config !== undefined && config !== null)
417
- const sourceId = typeof options.sourceId === 'string' && options.sourceId.length > 0
418
- ? options.sourceId
419
- : `preset:${String(list[0]?.id ?? 'unknown')}`
420
- // 互斥组仍在本来源内筛选;空来源也保留协调器迟到/HMR 接管。
421
- const effectiveList = effectivePromptConfigs(list)
494
+ if (coordinatorPromotion !== undefined && owner !== undefined && owner !== coordinatorPromotion) {
495
+ throw new TypeError('prompt-tool: pre-step sources must use the active coordinator promotion owner')
496
+ }
497
+ // 执行 owner 唯一:有来源 owner 就用它,否则由本次 apply 建 fallback pair;协调器存在时
498
+ // 批执行与注册统一用协调器 pair,协调器消失/替换时再切回 standalone/新 pair。
499
+ const fallbackBacked = owner === undefined
500
+ const fallbackPromotion = fallbackBacked
501
+ ? {
502
+ main: createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false }),
503
+ withSubagents: createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true }),
504
+ }
505
+ : undefined
506
+ const standalonePromotion = owner ?? fallbackPromotion
507
+ let activePromotion = coordinatorPromotion ?? standalonePromotion
508
+ let prepared = selected.map(source => ({ ...source, promotion: activePromotion }))
509
+ let feedsPromotion = fallbackBacked && activePromotion === fallbackPromotion
510
+ // 先验证固定效果的目标并完成 owner 解析;owner 不一致时不能留下 wireLayers 监听。
511
+ const releaseLayers = options.layers === false ? () => {} : wireLayers(ctx, selected.flatMap(source => source.configs)
512
+ .filter(config => config.layer !== 'pre-step'), warnOnce, { agent: options.agent })
422
513
  const injectedMemo = new Map()
423
- const main = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
424
- const withSubagents = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true })
425
514
  const observerDisposer = ctx.on('session/event', (session, event) => {
426
- main.observe(session, event)
427
- withSubagents.observe(session, event)
428
- // 独立路径的去重记账:只有宿主真正持久化的注入消息才确认投递(门控剥离的候选不算)。
515
+ if (feedsPromotion) {
516
+ activePromotion.main.observe(session, event)
517
+ activePromotion.withSubagents.observe(session, event)
518
+ }
429
519
  confirmDelivered(injectedMemo, session, event)
430
520
  })
431
-
432
- const warnOnce = createWarnOnce(ctx, name)
433
- const prepend = options.prepend === true || list.some((config) => config.prepend === true)
434
- const promotion = { main, withSubagents }
435
- const source = {
436
- configs: effectiveList,
437
- ruleActions: options.ruleActions ?? [],
438
- officialInstructions: options.officialInstructions === true,
521
+ // shortcut: prepend 只认 options 与 configs;纯 ruleAction 的 pre-step-filter 在无协调器的
522
+ // standalone 路径拿不到。出现第二个无协调器的 pre-step 消费者时,把「需要最外层」上收到动作语义再统一。
523
+ const prepend = options.prepend === true || prepared.some(source => source.configs.some(config => config.prepend === true))
524
+ const registrations = new Map()
525
+ let active = true
526
+ const releaseRegistrations = () => {
527
+ for (const dispose of registrations.values()) {
528
+ try { dispose() } catch {}
529
+ }
530
+ registrations.clear()
439
531
  }
440
- /** 已交给协调器的注册(仅管理路径非空)。 */
441
- let registration = null
442
- const releaseRegistration = () => {
443
- if (registration === null) return
444
- const dispose = registration
445
- registration = null
532
+ const setOwner = (promotion) => {
533
+ activePromotion = promotion
534
+ feedsPromotion = fallbackBacked && promotion === fallbackPromotion
535
+ prepared = selected.map(source => ({ ...source, promotion }))
536
+ }
537
+ const registerAll = (target) => {
446
538
  try {
447
- dispose()
448
- } catch {
449
- // 协调器自身已释放:忽略。
539
+ for (const source of prepared) {
540
+ if (registrations.has(source.sourceId)) throw new TypeError(`prompt-tool: duplicate pre-step sourceId ${source.sourceId}`)
541
+ registrations.set(source.sourceId, target.registerPreset(source.sourceId, source) ?? (() => {}))
542
+ }
543
+ } catch (error) {
544
+ releaseRegistrations()
545
+ throw error
450
546
  }
451
547
  }
452
- let active = true
453
- /** 释放本执行器:失效来源不再注入,也不再向新协调器重新注册。 */
454
- const release = () => {
455
- active = false
456
- releaseRegistration()
457
- injectedMemo.clear()
458
- }
459
- keepDisposer(ctx, release)
460
-
461
- // 非 pre-step 提示词配置接入各自声明的官方层级通道(system-section /
462
- // runtime-context / agent-request / llm-stream / tool-pipeline)。
463
- const releaseLayers = options.layers === false ? () => {}
464
- : wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
465
-
466
- // 协调器已在(正常装配顺序:插件先加载、预设后挂载)时立即登记来源;
467
- // 迟到/消失由下面的监听器按「先撤旧再启新」处理。
468
- let registeredCoordinator = coordinatorOf(ctx)
469
- if (registeredCoordinator !== undefined) {
470
- registration = registeredCoordinator.registerPreset(ctx, sourceId, source) ?? null
548
+ let registeredCoordinator = coordinator
549
+ if (coordinator !== undefined) {
550
+ try {
551
+ registerAll(coordinator)
552
+ } catch (error) {
553
+ try { observerDisposer() } catch {}
554
+ releaseLayers()
555
+ throw error
556
+ }
471
557
  }
472
-
473
558
  const preStepDisposer = ctx.on('agent/pre-step', async (payload, next) => {
474
- // waterfall 可已捕获随后被释放的回调;失效来源不得向新协调器重新注册。
475
559
  if (!active) return next()
476
- const coordinator = coordinatorOf(ctx)
477
- // HMR 可在相邻两步之间替换服务:旧句柄非空不代表仍向当前实例注册。
478
- if (coordinator !== registeredCoordinator) {
479
- releaseRegistration()
480
- if (coordinator === undefined) {
560
+ const current = coordinatorOf(ctx)
561
+ if (current !== registeredCoordinator) {
562
+ releaseRegistrations()
563
+ if (current === undefined) {
481
564
  warnOnce(`${name}: pre-step coordinator unavailable, resuming standalone execution`)
565
+ setOwner(standalonePromotion)
566
+ } else {
567
+ if (current.promotion === undefined) {
568
+ warnOnce(`${name}: pre-step coordinator v2 owner unavailable, keeping standalone execution`)
569
+ setOwner(standalonePromotion)
570
+ } else {
571
+ setOwner(current.promotion)
572
+ }
482
573
  }
483
- registeredCoordinator = coordinator
574
+ registeredCoordinator = current
484
575
  }
485
- if (coordinator !== undefined) {
486
- // 管理路径:先登记本来源,再交出唯一执行权(本监听器不再注入)。
487
- if (registration === null) registration = coordinator.registerPreset(ctx, sourceId, source) ?? null
576
+ if (current !== undefined && current.promotion !== undefined) {
577
+ if (registrations.size === 0) registerAll(current)
488
578
  return next()
489
579
  }
490
580
  const decision = await next()
491
581
  if (!active) return decision
492
582
  return runPreStepBatch({
583
+ sources: prepared,
493
584
  ctx,
494
585
  payload,
495
586
  agent: payload?.agent,
496
587
  decision,
497
- configs: effectiveList,
498
- ruleActions: source.ruleActions,
499
- promotion,
500
588
  memo: injectedMemo,
501
589
  warnOnce,
502
590
  })
503
591
  }, { prepend })
504
- // 释放边界:先摘监听器,再撤层级注册,最后丢弃去重快路径(顺序与 keepDisposer 内的 release 一致)。
592
+ const release = () => {
593
+ active = false
594
+ releaseRegistrations()
595
+ injectedMemo.clear()
596
+ }
597
+ keepDisposer(ctx, release)
505
598
  return () => {
506
599
  release()
507
- try {
508
- preStepDisposer()
509
- } catch {
510
- // 宿主已释放该 fiber 时监听器可能已不在:忽略。
511
- }
512
- try {
513
- observerDisposer()
514
- } catch {
515
- // 同上。
516
- }
600
+ try { preStepDisposer() } catch {}
601
+ try { observerDisposer() } catch {}
517
602
  releaseLayers()
518
603
  }
519
604
  }