dsh-plugin-prompt-tool 0.7.1 → 0.8.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 (169) hide show
  1. package/README.md +185 -46
  2. package/engine/THIRD_PARTY_LICENSES +25 -0
  3. package/engine/actions.mjs +931 -0
  4. package/engine/anchor-match.mjs +6 -3
  5. package/engine/character-tools.mjs +11 -0
  6. package/engine/classify-task.mjs +18 -0
  7. package/engine/compaction-epoch.mjs +41 -20
  8. package/engine/compositions/library/agent-instructions.yml +9 -0
  9. package/engine/compositions/library/command-goal.yml +2 -6
  10. package/engine/compositions/library/compaction.yml +2 -32
  11. package/engine/compositions/library/delegation-ptc.yml +58 -0
  12. package/engine/compositions/library/delegation.yml +5 -38
  13. package/engine/compositions/library/persistent-shell.yml +4 -13
  14. package/engine/compositions/library/planning.yml +8 -23
  15. package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
  16. package/engine/compositions/library/skill-filesystem.yml +2 -8
  17. package/engine/compositions/library/tool-ask-user.yml +2 -3
  18. package/engine/compositions/library/tool-bash.yml +4 -12
  19. package/engine/compositions/library/tool-cordis.yml +7 -0
  20. package/engine/compositions/library/tool-fs-search.yml +2 -11
  21. package/engine/compositions/library/tool-fs.yml +2 -5
  22. package/engine/compositions/library/tool-goal.yml +2 -6
  23. package/engine/compositions/library/tool-jobs.yml +2 -18
  24. package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
  25. package/engine/compositions/library/tool-plugin-manager.yml +8 -0
  26. package/engine/compositions/library/tool-present.yml +7 -0
  27. package/engine/compositions/library/tool-presentation.yml +9 -0
  28. package/engine/compositions/library/tool-pwsh.yml +2 -6
  29. package/engine/compositions/library/tool-skill.yml +7 -0
  30. package/engine/compositions/library/tool-todo.yml +2 -4
  31. package/engine/compositions/library/tool-web.yml +2 -1
  32. package/engine/compositions/source/local/character-tools.yml +5 -0
  33. package/engine/compositions/source/local/declared-triggers.yml +12 -0
  34. package/engine/compositions/source/local/filesystem-editor.yml +30 -0
  35. package/engine/compositions/source/local/instruction-hint.yml +15 -0
  36. package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
  37. package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
  38. package/engine/compositions/source/local/run-code-env.yml +4 -7
  39. package/engine/compositions/source/local/session-var-tools.yml +5 -0
  40. package/engine/compositions/source/local/skill-search.yml +3 -9
  41. package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
  42. package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
  43. package/engine/compositions/source/local/tool-config-engine.yml +8 -0
  44. package/engine/compositions/source/local/tool-git-bash.yml +15 -0
  45. package/engine/compositions/source/local/world-book-tools.yml +5 -0
  46. package/engine/condition.mjs +83 -0
  47. package/engine/declared-triggers.mjs +93 -0
  48. package/engine/executor.mjs +364 -185
  49. package/engine/fields.mjs +219 -0
  50. package/engine/fillers.mjs +12 -141
  51. package/engine/host-package.mjs +51 -0
  52. package/engine/instruction-hint.mjs +285 -0
  53. package/engine/interpolate.mjs +157 -29
  54. package/engine/layers.mjs +495 -75
  55. package/engine/predicates.mjs +753 -0
  56. package/engine/prompt-config-engine.mjs +63 -8
  57. package/engine/run-code-env.mjs +48 -31
  58. package/engine/schema.mjs +444 -103
  59. package/engine/sdk-strip.mjs +365 -0
  60. package/engine/session-var-tools.mjs +11 -0
  61. package/engine/session-vars.mjs +8 -6
  62. package/engine/shared.mjs +137 -0
  63. package/engine/skill-search.mjs +34 -21
  64. package/engine/st-macros.d.mts +22 -0
  65. package/engine/st-macros.mjs +208 -0
  66. package/engine/st-render.mjs +105 -0
  67. package/engine/st-world-book.d.mts +10 -0
  68. package/engine/st-world-book.mjs +259 -0
  69. package/engine/strategies.mjs +31 -20
  70. package/engine/subagent-tool-policy-core.mjs +400 -0
  71. package/engine/subagent-tool-policy.mjs +327 -0
  72. package/engine/tool-config-engine.mjs +207 -145
  73. package/engine/tool-definition.mjs +143 -0
  74. package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
  75. package/engine/trigger-editor-meta.mjs +48 -0
  76. package/engine/trigger-spec.mjs +257 -0
  77. package/engine/trigger.mjs +304 -0
  78. package/engine/world-book-tools.mjs +11 -0
  79. package/lib/client.js +16495 -5456
  80. package/lib/client.js.map +1 -1
  81. package/lib/index.d.mts +628 -382
  82. package/lib/index.mjs +5391 -2942
  83. package/lib/paths-CY5psBqE.d.mts +1233 -0
  84. package/lib/preset-package-BnJqrU4R.mjs +16792 -0
  85. package/lib/preset-transfer.d.mts +2 -0
  86. package/lib/preset-transfer.mjs +2 -0
  87. package/package.json +91 -49
  88. package/preset/pt-cordis/preset.yml +43 -0
  89. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
  90. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
  91. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
  92. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
  93. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
  94. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
  95. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
  96. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
  97. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
  98. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
  99. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
  100. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
  101. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
  102. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
  103. package/preset/pt-custom/preset.yml +15 -0
  104. package/preset/pt-minimal/preset.yml +27 -0
  105. package/preset/pt-ptc/preset.yml +45 -0
  106. package/preset/pt-standard/preset.yml +43 -0
  107. package/templates/14-first-turn-anchor.yml +4 -4
  108. package/templates/15-guide-auto.yml +3 -3
  109. package/templates/16-custom-fallback.yml +1 -0
  110. package/templates/18-placeholder-env-facts.yml +3 -3
  111. package/templates/20-system-section.yml +3 -2
  112. package/templates/65-turn-stop.yml +18 -0
  113. package/templates/66-subagent-start.yml +14 -0
  114. package/templates/75-subagent-end.yml +15 -0
  115. package/templates/tools/10-shell-command.yml +1 -1
  116. package/templates/tools/40-delegate-character.yml +1 -1
  117. package/templates/tools/50-delegate-worldbook.yml +1 -1
  118. package/templates/tools/60-delegate-session-var.yml +1 -1
  119. package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
  120. package/engine/anchor-turn.mjs +0 -68
  121. package/engine/code-presentation.mjs +0 -103
  122. package/engine/compositions/library/anchor-turn.yml +0 -10
  123. package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
  124. package/engine/compositions/library/code-presentation.yml +0 -8
  125. package/engine/compositions/library/context-gate.yml +0 -39
  126. package/engine/compositions/library/cot-drip.yml +0 -11
  127. package/engine/compositions/library/custom-bash.yml +0 -14
  128. package/engine/compositions/library/deliberation-gate.yml +0 -11
  129. package/engine/compositions/library/official-agent-instructions.yml +0 -17
  130. package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
  131. package/engine/compositions/library/official-tool-bash.yml +0 -6
  132. package/engine/compositions/library/official-tool-cordis.yml +0 -13
  133. package/engine/compositions/library/official-tool-presentation.yml +0 -7
  134. package/engine/compositions/library/official-tool-skill.yml +0 -14
  135. package/engine/compositions/library/persona.yml +0 -13
  136. package/engine/compositions/library/prompt-config-engine.yml +0 -16
  137. package/engine/compositions/library/run-code-env.yml +0 -15
  138. package/engine/compositions/library/skill-search.yml +0 -14
  139. package/engine/compositions/library/str-replace-editor.yml +0 -10
  140. package/engine/compositions/library/tool-bootstrap.yml +0 -14
  141. package/engine/compositions/library/tool-config-engine.yml +0 -9
  142. package/engine/compositions/library/tool-filter.yml +0 -8
  143. package/engine/compositions/source/local/code-presentation.yml +0 -8
  144. package/engine/compositions/source/local/context-gate.yml +0 -36
  145. package/engine/compositions/source/local/custom-bash.yml +0 -11
  146. package/engine/compositions/source/local/str-replace-editor.yml +0 -10
  147. package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
  148. package/engine/compositions/source/local/tool-filter.yml +0 -8
  149. package/engine/context-gate.mjs +0 -305
  150. package/engine/cot-drip.mjs +0 -137
  151. package/engine/deliberation-gate.mjs +0 -127
  152. package/engine/tool-bootstrap.mjs +0 -476
  153. package/engine/tool-filter.mjs +0 -80
  154. package/lib/preset-core.d.mts +0 -10
  155. package/lib/preset-core.mjs +0 -77
  156. package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
  157. package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
  158. package/preset/anchored/preset.yml +0 -296
  159. package/preset/creative/preset.yml +0 -67
  160. package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
  161. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
  162. package/preset/custom/preset.yml +0 -12
  163. package/preset/liangshen/preset.yml +0 -75
  164. package/preset/minimal/preset.yml +0 -45
  165. package/preset/ptc/preset.yml +0 -58
  166. package/preset/standard/preset.yml +0 -57
  167. package/skills/manifest.json +0 -7
  168. package/skills/sandboxmod/SKILL.md +0 -49
  169. package/skills/web ui/SKILL.md +0 -42
@@ -2,22 +2,50 @@
2
2
  * executor — pre-step 消息批执行器。
3
3
  * 职责:过滤(层/子代理/模型/晋升)→ resolve → 插值 → 去重/合并 → 落位插入。
4
4
  * 任一提示词配置失败只跳过该提示词配置并 warnOnce,绝不让注入 bug 卡死会话。
5
+ *
6
+ * 装配路径有两条,批执行算法只有一份(runPreStepBatch):
7
+ * - 管理路径:宿主 prompt-tool 插件提供 `promptToolPreStep` 协调服务时,本来源
8
+ * (预设 mount)把配置注册给协调器,由协调器统一执行预设来源与独立指令文件来源;
9
+ * - 独立路径:没有协调服务(引擎被复制到无宿主的目录复用)时,本行 ctx 注册本地
10
+ * pre-step 监听器,只执行自身预设配置。
11
+ * 协调服务迟到或消失时按「先撤旧再启新」切换,任一时刻每 scope 只有一条执行路径。
5
12
  */
6
13
 
7
14
  import {
15
+ MAX_TRACKED_SESSIONS,
8
16
  PROMOTE_EVENTS,
9
17
  createWarnOnce,
18
+ getService,
10
19
  isDelegated,
20
+ keepDisposer,
11
21
  matchesModel,
12
22
  newMessageId,
23
+ sessionEvents,
13
24
  } from './shared.mjs'
14
25
  import { interpolateVariables } from './interpolate.mjs'
26
+ import { conditionHit, userMessagesText } from './condition.mjs'
15
27
  import { createEpochPromotion } from './compaction-epoch.mjs'
16
28
  import { wireLayers } from './layers.mjs'
17
29
  import { sessionVarsSnapshot } from './session-vars.mjs'
30
+ import { selectStWorldBook } from './st-world-book.mjs'
18
31
 
19
32
  const name = 'prompt-config-engine'
20
33
 
34
+ /** 已确认投递身份的进程内快路径上限;真相在持久事件流。 */
35
+ // ponytail: 身份条数上限防无界增长(Map 按投递身份累积);若需精确淘汰改 LRU。
36
+ const MAX_MEMO_CONFIGS = 4096
37
+
38
+ /**
39
+ * 查询宿主 prompt-tool 插件的 pre-step 协调服务(ctx.get 跨 fiber 解析)。
40
+ * 缺失/形状不符时返回 undefined:本行回退独立执行路径。
41
+ */
42
+ function coordinatorOf(ctx) {
43
+ const service = getService(ctx, 'promptToolPreStep')
44
+ return service !== null && typeof service === 'object' && typeof service.registerPreset === 'function'
45
+ ? service
46
+ : undefined
47
+ }
48
+
21
49
  /** 事件数据兼容两层形状:event.data 本身是消息,或 event.data.message 是消息。 */
22
50
  function eventMessage(event) {
23
51
  const data = event?.data
@@ -32,9 +60,14 @@ function mergedIdentity(config) {
32
60
  return `merged:${config.position}`
33
61
  }
34
62
 
63
+ /** 去重身份:merged 组用位置命名空间,独立配置用自身身份。 */
64
+ function identityOf(config) {
65
+ return config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
66
+ }
67
+
35
68
  function hasInjected(config, session) {
36
- const value = config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
37
- return (Array.isArray(session.events) ? session.events : []).some((event) => {
69
+ const value = identityOf(config)
70
+ return sessionEvents(session).some((event) => {
38
71
  const message = eventMessage(event)
39
72
  // 双通道去重:kind(外来/第三方消息,如 context-gate 的 instruction-hint)或
40
73
  // plugin(本引擎注入的命名空间,merged 组用 merged:<position>)。
@@ -44,57 +77,280 @@ function hasInjected(config, session) {
44
77
 
45
78
  /** 当前消息批内是否已有该提示词配置注入(每轮去重)。 */
46
79
  function hasInBatch(config, messages) {
47
- const value = config.mergeMode === 'merged' ? mergedIdentity(config) : config.identity.value
80
+ const value = identityOf(config)
48
81
  return messages.some((message) =>
49
82
  message?.source?.kind === config.sourceKind || message?.source?.plugin === value)
50
83
  }
51
84
 
52
- /** 构造默认 user/assistant 消息;策略返回完整 patch 时覆盖对应字段。 */
53
- function buildMessage(config, resolved) {
85
+ /**
86
+ * 按投递身份取「已确认投递」的会话集合。记账只发生在宿主真正接纳并持久化该消息之后
87
+ * (见 confirmDelivered):候选在瀑布外层被门控/策略剥离时不算已投递,否则晋升后
88
+ * (门控放行时)正文会永久缺失。集合上限按会话数截断,真相仍在持久事件流。
89
+ */
90
+ function deliveredSessions(memo, key) {
91
+ let set = memo.get(key)
92
+ if (set === undefined) {
93
+ if (memo.size >= MAX_MEMO_CONFIGS) memo.clear()
94
+ set = new Set()
95
+ memo.set(key, set)
96
+ }
97
+ if (set.size >= MAX_TRACKED_SESSIONS) set.clear()
98
+ return set
99
+ }
100
+
101
+ /**
102
+ * 记录一次宿主已接纳的注入消息(由调用方在 session/event 里转发)。
103
+ * plugin(本引擎身份,merged 组用 merged:<position>)与 kind(外来通道,如
104
+ * context-gate 的 instruction-hint)都以会话为界记账,与 hasInjected 的双通道语义一致。
105
+ */
106
+ export function confirmDelivered(memo, session, event) {
107
+ if (memo === null || memo === undefined || session === null || session === undefined) return
108
+ const source = eventMessage(event)?.source
109
+ if (source === null || typeof source !== 'object') return
110
+ for (const field of ['plugin', 'kind']) {
111
+ const value = source[field]
112
+ if (typeof value !== 'string' || value.length === 0) continue
113
+ deliveredSessions(memo, `${field}:${value}`).add(session.id)
114
+ }
115
+ }
116
+
117
+ /** dedupe=session:本会话是否已有该身份的已确认投递(快路径 + 持久事件真相)。 */
118
+ function alreadyDelivered(config, session, memo) {
119
+ const confirmed = (field, value) => memo.get(`${field}:${value}`)?.has(session.id) === true
120
+ if (confirmed('plugin', identityOf(config))) return true
121
+ if (typeof config.sourceKind === 'string' && confirmed('kind', config.sourceKind)) return true
122
+ return hasInjected(config, session)
123
+ }
124
+
125
+ /**
126
+ * pre-step 唯一可发出角色:宿主把本批消息逐条写成 `user/message` 事件,事件校验要求
127
+ * `role === 'user'`(assistant 只能由模型侧 assistant/message 事件产生)。
128
+ */
129
+ const PRE_STEP_ROLE = 'user'
130
+
131
+ /**
132
+ * 记录一次角色降级:保留原角色、明确告警、不改正文,也不把非法值传给宿主。
133
+ * 同一配置只告警一次(warnOnce),且不记录正文或用户内容。
134
+ */
135
+ function downgradeRole(config, requested, warnOnce) {
136
+ if (typeof requested !== 'string' || requested === PRE_STEP_ROLE) return undefined
137
+ warnOnce(`${name}: config ${String(config?.id ?? '<unknown>')} role ${JSON.stringify(requested)} cannot be injected in pre-step, message downgraded to "${PRE_STEP_ROLE}"`)
138
+ return requested
139
+ }
140
+
141
+ /** 构造默认 user 消息;策略返回完整 patch 时覆盖对应字段,但非法角色在出口兜底降级。 */
142
+ function buildMessage(config, resolved, warnOnce) {
54
143
  const text = typeof resolved.text === 'string' ? resolved.text : ''
55
144
  const defaultContent = config.texts.length > 0
56
145
  ? config.texts.map((item) => ({ type: 'text', text: item }))
57
146
  : [{ type: 'text', text }]
58
147
  const sourceValue = mergedIdentity(config)
148
+ // 策略 patch(templateFile 的 role 等)与配置声明都必须经过同一出口判定。
149
+ const requested = downgradeRole(config, typeof resolved.role === 'string' ? resolved.role : config.role, warnOnce)
150
+ const base = resolved.source !== null && typeof resolved.source === 'object'
151
+ ? resolved.source
152
+ : {
153
+ kind: config.sourceKind,
154
+ plugin: sourceValue,
155
+ ...(typeof config.form === 'string' ? { form: config.form } : {}),
156
+ ...(typeof config.summary === 'string' && config.summary.length > 0 ? { summary: config.summary } : {}),
157
+ }
59
158
  return {
60
159
  id: typeof resolved.id === 'string' && resolved.id.length > 0 ? resolved.id : newMessageId(config.id),
61
- role: typeof resolved.role === 'string' ? resolved.role : config.role,
160
+ role: PRE_STEP_ROLE,
62
161
  content: Array.isArray(resolved.content) ? resolved.content : defaultContent,
63
- source: resolved.source !== null && typeof resolved.source === 'object'
64
- ? resolved.source
65
- : {
66
- kind: config.sourceKind,
67
- plugin: sourceValue,
68
- ...(typeof config.form === 'string' ? { form: config.form } : {}),
69
- ...(typeof config.summary === 'string' && config.summary.length > 0 ? { summary: config.summary } : {}),
70
- },
162
+ source: requested === undefined ? base : { ...base, requestedRole: requested },
71
163
  }
72
164
  }
73
165
 
74
- /** 按提示词配置声明的插入位置写入消息批;找不到插入锚点返回 false(本提示词配置跳过)。 */
75
- function insertMessage(config, messages, message) {
76
- if (config.position === 'before-all') {
77
- messages.unshift(message)
78
- return true
79
- }
80
- if (config.position === 'after-all') {
81
- messages.push(message)
82
- return true
166
+ /**
167
+ * 批执行算法(单一实现):过滤(层/子代理/模型/晋升)→ resolve → 插值 →
168
+ * 去重/合并 → 落位插入。
169
+ *
170
+ * 预设卡与独立指令文件卡共用这条路径,身份空间互不覆盖;文件来源的候选资格由
171
+ * 调用方(协调器)按可见状态先行判定,这里只按各卡自身声明的策略执行。文件正文以
172
+ * content 块返回(不填 config.texts),因此不经过预设变量插值。
173
+ *
174
+ * @param options.configs 已过滤 enabled 与互斥组的最终提示词配置列表。
175
+ * @param options.promotion { main, withSubagents } 晋升追踪器(真相在持久事件流)。
176
+ * @param options.memo 已确认投递身份的进程内 session 去重快路径(见 confirmDelivered)。
177
+ * @returns 注入后的 decision;reject、缺 agent/session、全部跳过或异常时原样返回。
178
+ */
179
+ export async function runPreStepBatch(options) {
180
+ const { ctx, agent, decision, configs, promotion, memo, warnOnce } = options
181
+ if (decision === null || typeof decision !== 'object') return decision
182
+ if (decision.kind === 'reject') return decision
183
+ if (agent === undefined) return decision
184
+ const session = agent.session
185
+ if (session === undefined) return decision
186
+ const { main, withSubagents } = promotion
187
+ try {
188
+ const messages = Array.isArray(decision.messages) ? [...decision.messages] : []
189
+ // 条件判定的匹配对象在本批进入时取定:批次内后续注入不改变本批的判定依据。
190
+ const userText = userMessagesText(messages)
191
+ let changed = false
192
+
193
+ const due = []
194
+ // 本批资格判定(唯一实现):层通道、受众、模型、晋升与声明式条件。
195
+ // 条件判定放在去重之前:未命中的配置不算「已注入」,条件恢复后仍应能注入。
196
+ // ST 模板的跨配置变量帧按 order 预求值,但只有这里的获准集合才允许产生副作用
197
+ // (未命中的 setter 提前 setvar 会污染同批 reader)——渲染器不再复制判定。
198
+ const delegated = isDelegated(session)
199
+ const qualified = (config) => config.layer === 'pre-step'
200
+ && !(config.audience === 'main' && delegated)
201
+ && !(config.audience === 'subagent' && !delegated)
202
+ && matchesModel(config.modelScope, agent.options?.model)
203
+ && (config.promotion !== 'main' || main.status(agent).promoted)
204
+ && (config.promotion !== 'include-subagents' || withSubagents.status(agent).promoted)
205
+ && conditionHit(config, { userText })
206
+ const qualifiedConfigs = configs.filter(qualified)
207
+ // 协调器为绑定来源 ctx 会复制 config;renderSt 函数身份在副本间保持不变。
208
+ const eligible = new Set(qualifiedConfigs.map(config => config.renderSt))
209
+ const stWorldBook = selectStWorldBook(qualifiedConfigs, session, messages, warnOnce)
210
+ for (const config of configs) {
211
+ try {
212
+ if (!qualified(config)) continue
213
+
214
+ if (config.dedupe === 'session') {
215
+ if (alreadyDelivered(config, session, memo)) continue
216
+ } else if (config.dedupe === 'batch' && hasInBatch(config, messages)) {
217
+ continue
218
+ }
219
+
220
+ const resolved = await config.resolve({ ctx, agent, session, decision, messages, stWorldBookSelected: stWorldBook.has(config) })
221
+ if (resolved === null || resolved === undefined) continue
222
+ const patched = { ...resolved }
223
+ // params 并入插值变量:ST 变量(setvar/getvar 收集 + 预设参数)顶层 key 直接可插值
224
+ //(含中文 key 如 {{接受值}}——引擎正则已支持 Unicode 字母)。
225
+ // 会话变量(session_var 工具维护)覆盖配置/预设默认:resolved > 会话 > params > 配置。
226
+ const mergedVars = {
227
+ ...config.variables,
228
+ ...config.params,
229
+ ...sessionVarsSnapshot(session),
230
+ ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}),
231
+ }
232
+ if (typeof config.renderSt === 'function') {
233
+ patched.text = config.renderSt(agent, messages, warnOnce, options, eligible)
234
+ patched.content = patched.text.length > 0 ? [{ type: 'text', text: patched.text }] : []
235
+ } else if (typeof patched.text === 'string') {
236
+ // 提示词配置级模板变量 + filler 变量 + 内置环境变量插值。
237
+ patched.text = interpolateVariables(patched.text, mergedVars, session)
238
+ }
239
+ if (config.texts.length > 0 && typeof config.renderSt !== 'function') {
240
+ const blocks = config.texts
241
+ .map((item) => interpolateVariables(item, mergedVars, session))
242
+ .filter((item) => item.length > 0)
243
+ .map((item) => ({ type: 'text', text: item }))
244
+ if (blocks.length > 0) patched.content = blocks
245
+ }
246
+ const hasText = typeof patched.text === 'string' && patched.text.length > 0
247
+ const hasContent = Array.isArray(patched.content) && patched.content.length > 0
248
+ if (!hasText && !hasContent) continue
249
+ due.push({ config, resolved: patched })
250
+ } catch (error) {
251
+ // 单个提示词配置失败不得伤及会话:跳过该提示词配置,只告警一次。
252
+ warnOnce(`${name}: config ${String(config?.id ?? '<unknown>')} failed, skipping: ${String((error && error.message) || error)}`)
253
+ }
254
+ }
255
+
256
+ // order 升序(同值保持声明顺序):决定拼接顺序与同位置插入顺序。
257
+ due.sort((a, b) => a.config.order - b.config.order)
258
+
259
+ // merged 模式的多条提示词配置在首条配置的位置合并为一条消息;
260
+ // 文本按内容块拼接,source 身份改用 merged:<position> 以保持持久幂等。
261
+ const orderedGroups = []
262
+ const groupIndex = new Map()
263
+ for (const entry of due) {
264
+ const group = entry.config.mergeMode === 'merged'
265
+ ? mergedIdentity(entry.config)
266
+ : undefined
267
+ if (group === undefined) {
268
+ orderedGroups.push([entry])
269
+ continue
270
+ }
271
+ let index = groupIndex.get(group)
272
+ if (index === undefined) {
273
+ index = orderedGroups.length
274
+ groupIndex.set(group, index)
275
+ orderedGroups.push([])
276
+ }
277
+ orderedGroups[index].push(entry)
278
+ }
279
+
280
+ const planned = []
281
+ for (const group of orderedGroups) {
282
+ const base = group[0]
283
+ const message = buildMessage(base.config, base.resolved, warnOnce)
284
+ if (group.length > 1) {
285
+ message.content = group.flatMap((entry) => {
286
+ if (Array.isArray(entry.resolved.content)) return entry.resolved.content
287
+ return typeof entry.resolved.text === 'string' && entry.resolved.text.length > 0
288
+ ? [{ type: 'text', text: entry.resolved.text }]
289
+ : []
290
+ })
291
+ if (message.source !== null && typeof message.source === 'object') {
292
+ message.source = { ...message.source, plugin: mergedIdentity(base.config) }
293
+ }
294
+ // 合并组的角色由首条配置决定;其余成员声明的非法角色同样要告警并留下降级事实。
295
+ for (const entry of group.slice(1)) {
296
+ const requested = downgradeRole(entry.config, typeof entry.resolved.role === 'string' ? entry.resolved.role : entry.config.role, warnOnce)
297
+ if (requested !== undefined && message.source.requestedRole === undefined) {
298
+ message.source = { ...message.source, requestedRole: requested }
299
+ }
300
+ }
301
+ if (new Set(group.map((entry) => entry.config.position)).size > 1) {
302
+ warnOnce(`${name}: merged group mixes positions — using ${String(base.config.position)} from the first config`)
303
+ }
304
+ }
305
+ planned.push({ position: base.config.position, message, group })
306
+ }
307
+
308
+ // 同位置批量插入:planned 已按 order 升序,多元素 splice/unshift/push 保持该顺序。
309
+ // 去重记账不在这里:候选被外层门控剥离时也必须保持「未投递」,由 confirmDelivered 确认。
310
+ const markGroup = (group) => {
311
+ for (const entry of group) stWorldBook.commit?.(entry.config)
312
+ }
313
+ const beforeAll = planned.filter((item) => item.position === 'before-all')
314
+ const afterUser = planned.filter((item) => item.position !== 'before-all' && item.position !== 'after-all')
315
+ const afterAll = planned.filter((item) => item.position === 'after-all')
316
+ if (beforeAll.length > 0) {
317
+ messages.unshift(...beforeAll.map((item) => item.message))
318
+ for (const item of beforeAll) markGroup(item.group)
319
+ changed = true
320
+ }
321
+ const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
322
+ if (afterUser.length > 0 && userIndex >= 0) {
323
+ messages.splice(userIndex + 1, 0, ...afterUser.map((item) => item.message))
324
+ for (const item of afterUser) markGroup(item.group)
325
+ changed = true
326
+ }
327
+ if (afterAll.length > 0) {
328
+ messages.push(...afterAll.map((item) => item.message))
329
+ for (const item of afterAll) markGroup(item.group)
330
+ changed = true
331
+ }
332
+
333
+ return changed ? { ...decision, messages } : decision
334
+ } catch (error) {
335
+ warnOnce(`${name}: prompt config failed, skipping: ${String((error && error.message) || error)}`)
336
+ return decision
83
337
  }
84
- // 默认 after-user:只锚真实用户消息,插件消息不算。
85
- const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
86
- if (userIndex < 0) return false
87
- messages.splice(userIndex + 1, 0, message)
88
- return true
89
338
  }
90
339
 
91
340
  /**
92
- * 把一组运行时提示词配置装配为注入执行器。
93
- * @param prepend 是否以 prepend 注册 pre-step(合并行恒 true;单条提示词配置兼容层由参数决定)。
341
+ * 把一组运行时提示词配置装配为注入执行器(独立模式 / 协调器管理模式的接线口)。
342
+ *
343
+ * @param options.prepend 是否以 prepend 注册本地 pre-step(合并行恒 true;由参数决定)。
344
+ * @param options.sourceId 协调器里的来源 id(默认取首条配置 id 派生)。
345
+ * @param options.officialInstructions 本 mount 的组合是否仍挂着官方指令加载行
346
+ * (负责人冲突事实;协调器据此拒绝同时注入文件正文)。
94
347
  */
95
348
  export function applyPromptConfigs(ctx, configs, options = {}) {
96
349
  const list = configs.filter((config) => config !== undefined && config !== null)
97
- if (list.length === 0) return
350
+ const sourceId = typeof options.sourceId === 'string' && options.sourceId.length > 0
351
+ ? options.sourceId
352
+ : `preset:${String(list[0]?.id ?? 'unknown')}`
353
+ // 空 mount 同样保留接管监听:装配事实与协调器迟到/HMR 不依赖是否有预设卡。
98
354
  // 互斥组:同一 group 且 exclusive=true 时,只保留排序后第一个 enabled 提示词配置。
99
355
  const claimedGroups = new Set()
100
356
  const effectiveList = list.filter((config) => {
@@ -105,174 +361,97 @@ export function applyPromptConfigs(ctx, configs, options = {}) {
105
361
  }
106
362
  return true
107
363
  })
364
+ const injectedMemo = new Map()
108
365
  const main = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
109
366
  const withSubagents = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: true })
110
- ctx.on('session/event', (session, event) => {
367
+ const observerDisposer = ctx.on('session/event', (session, event) => {
111
368
  main.observe(session, event)
112
369
  withSubagents.observe(session, event)
370
+ // 独立路径的去重记账:只有宿主真正持久化的注入消息才确认投递(门控剥离的候选不算)。
371
+ confirmDelivered(injectedMemo, session, event)
113
372
  })
114
373
 
115
- /** 每提示词配置每会话的进程内快路径;真相在持久事件流。 */
116
- // ponytail: 会话数上限防无界增长(Set 按 session.id 累积);若需精确淘汰改 LRU。
117
- const MAX_MEMO_SESSIONS = 4096
118
- const injectedMemo = new Map()
119
- const configMemo = (config) => {
120
- let memo = injectedMemo.get(config.id)
121
- if (memo === undefined) {
122
- if (injectedMemo.size >= MAX_MEMO_SESSIONS) injectedMemo.clear()
123
- memo = new Set()
124
- injectedMemo.set(config.id, memo)
125
- }
126
- return memo
127
- }
128
-
129
374
  const warnOnce = createWarnOnce(ctx, name)
130
375
  const prepend = options.prepend === true || list.some((config) => config.prepend === true)
376
+ const promotion = { main, withSubagents }
377
+ const source = {
378
+ configs: effectiveList,
379
+ officialInstructions: options.officialInstructions === true,
380
+ }
381
+ /** 已交给协调器的注册(仅管理路径非空)。 */
382
+ let registration = null
383
+ const releaseRegistration = () => {
384
+ if (registration === null) return
385
+ const dispose = registration
386
+ registration = null
387
+ try {
388
+ dispose()
389
+ } catch {
390
+ // 协调器自身已释放:忽略。
391
+ }
392
+ }
393
+ let active = true
394
+ /** 释放本执行器:失效来源不再注入,也不再向新协调器重新注册。 */
395
+ const release = () => {
396
+ active = false
397
+ releaseRegistration()
398
+ injectedMemo.clear()
399
+ }
400
+ keepDisposer(ctx, release)
131
401
 
132
402
  // 非 pre-step 提示词配置接入各自声明的官方层级通道(system-section /
133
403
  // runtime-context / agent-request / llm-stream / tool-pipeline)。
134
- wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
135
-
136
- ctx.on('agent/pre-step', async ({ agent }, next) => {
137
- const decision = await next()
138
- try {
139
- if (decision.kind === 'reject') return decision
140
- if (agent === undefined) return decision
141
- const session = agent.session
142
- if (session === undefined) return decision
143
-
144
- const messages = Array.isArray(decision.messages) ? [...decision.messages] : []
145
- let changed = false
404
+ const releaseLayers = wireLayers(ctx, effectiveList.filter((config) => config.layer !== 'pre-step'), warnOnce)
146
405
 
147
- const due = []
148
- for (const config of effectiveList) {
149
- try {
150
- if (config.layer !== 'pre-step') continue
151
- const delegated = isDelegated(session)
152
- if (config.audience === 'main' && delegated) continue
153
- if (config.audience === 'subagent' && !delegated) continue
154
- if (!matchesModel(config.modelScope, agent.options?.model)) continue
155
- if (config.promotion === 'main' && !main.status(agent).promoted) continue
156
- if (config.promotion === 'include-subagents' && !withSubagents.status(agent).promoted) continue
157
-
158
- const memo = configMemo(config)
159
- if (config.dedupe === 'session') {
160
- if (memo.has(session.id) || hasInjected(config, session)) {
161
- memo.add(session.id)
162
- continue
163
- }
164
- } else if (config.dedupe === 'batch' && hasInBatch(config, messages)) {
165
- continue
166
- }
167
-
168
- const resolved = await config.resolve({ ctx, agent, session, decision, messages })
169
- if (resolved === null || resolved === undefined) continue
170
- const patched = { ...resolved }
171
- // params 并入插值变量:ST 变量(setvar/getvar 收集 + 预设参数)顶层 key 直接可插值
172
- //(含中文 key 如 {{接受值}}——引擎正则已支持 Unicode 字母)。
173
- // 会话变量(session_var 工具维护)覆盖配置/预设默认:resolved > 会话 > params > 配置。
174
- const mergedVars = {
175
- ...config.variables,
176
- ...config.params,
177
- ...sessionVarsSnapshot(session),
178
- ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}),
179
- }
180
- if (typeof patched.text === 'string') {
181
- // 提示词配置级模板变量 + filler 变量 + 内置环境变量插值。
182
- patched.text = interpolateVariables(patched.text, mergedVars, session)
183
- }
184
- if (config.texts.length > 0) {
185
- const blocks = config.texts
186
- .map((item) => interpolateVariables(item, mergedVars, session))
187
- .filter((item) => item.length > 0)
188
- .map((item) => ({ type: 'text', text: item }))
189
- if (blocks.length > 0) patched.content = blocks
190
- }
191
- const hasText = typeof patched.text === 'string' && patched.text.length > 0
192
- const hasContent = Array.isArray(patched.content) && patched.content.length > 0
193
- if (!hasText && !hasContent) continue
194
- due.push({ config, resolved: patched })
195
- } catch (error) {
196
- // 单个提示词配置失败不得伤及会话:跳过该提示词配置,只告警一次。
197
- warnOnce(`${name}: config ${String(config?.id ?? '<unknown>')} failed, skipping: ${String((error && error.message) || error)}`)
198
- }
199
- }
200
-
201
- // order 升序(同值保持声明顺序):决定拼接顺序与同位置插入顺序。
202
- due.sort((a, b) => a.config.order - b.config.order)
203
-
204
- // merged 模式的多条提示词配置在首条配置的位置合并为一条消息;
205
- // 文本按内容块拼接,source 身份改用 merged:<position> 以保持持久幂等。
206
- const orderedGroups = []
207
- const groupIndex = new Map()
208
- for (const entry of due) {
209
- const group = entry.config.mergeMode === 'merged'
210
- ? mergedIdentity(entry.config)
211
- : undefined
212
- if (group === undefined) {
213
- orderedGroups.push([entry])
214
- continue
215
- }
216
- let index = groupIndex.get(group)
217
- if (index === undefined) {
218
- index = orderedGroups.length
219
- groupIndex.set(group, index)
220
- orderedGroups.push([])
221
- }
222
- orderedGroups[index].push(entry)
223
- }
224
-
225
- const planned = []
226
- for (const group of orderedGroups) {
227
- const base = group[0]
228
- const message = buildMessage(base.config, base.resolved)
229
- if (group.length > 1) {
230
- message.content = group.flatMap((entry) => {
231
- if (Array.isArray(entry.resolved.content)) return entry.resolved.content
232
- return typeof entry.resolved.text === 'string' && entry.resolved.text.length > 0
233
- ? [{ type: 'text', text: entry.resolved.text }]
234
- : []
235
- })
236
- if (message.source !== null && typeof message.source === 'object') {
237
- message.source = { ...message.source, plugin: mergedIdentity(base.config) }
238
- }
239
- if (new Set(group.map((entry) => entry.config.position)).size > 1) {
240
- warnOnce(`${name}: merged group mixes positions — using ${String(base.config.position)} from the first config`)
241
- }
242
- }
243
- planned.push({ position: base.config.position, message, group })
244
- }
406
+ // 协调器已在(正常装配顺序:插件先加载、预设后挂载)时立即登记来源;
407
+ // 迟到/消失由下面的监听器按「先撤旧再启新」处理。
408
+ let registeredCoordinator = coordinatorOf(ctx)
409
+ if (registeredCoordinator !== undefined) {
410
+ registration = registeredCoordinator.registerPreset(ctx, sourceId, source) ?? null
411
+ }
245
412
 
246
- // 同位置批量插入:planned 已按 order 升序,多元素 splice/unshift/push 保持该顺序。
247
- const markGroup = (group) => {
248
- for (const entry of group) {
249
- if (entry.config.dedupe === 'session') configMemo(entry.config).add(session.id)
250
- }
251
- }
252
- const beforeAll = planned.filter((item) => item.position === 'before-all')
253
- const afterUser = planned.filter((item) => item.position !== 'before-all' && item.position !== 'after-all')
254
- const afterAll = planned.filter((item) => item.position === 'after-all')
255
- if (beforeAll.length > 0) {
256
- messages.unshift(...beforeAll.map((item) => item.message))
257
- for (const item of beforeAll) markGroup(item.group)
258
- changed = true
259
- }
260
- const userIndex = messages.findIndex((item) => item?.source?.kind === 'user')
261
- if (afterUser.length > 0 && userIndex >= 0) {
262
- messages.splice(userIndex + 1, 0, ...afterUser.map((item) => item.message))
263
- for (const item of afterUser) markGroup(item.group)
264
- changed = true
413
+ const preStepDisposer = ctx.on('agent/pre-step', async (payload, next) => {
414
+ // waterfall 可已捕获随后被释放的回调;失效来源不得向新协调器重新注册。
415
+ if (!active) return next()
416
+ const coordinator = coordinatorOf(ctx)
417
+ // HMR 可在相邻两步之间替换服务:旧句柄非空不代表仍向当前实例注册。
418
+ if (coordinator !== registeredCoordinator) {
419
+ releaseRegistration()
420
+ if (coordinator === undefined) {
421
+ warnOnce(`${name}: pre-step coordinator unavailable, resuming standalone execution`)
265
422
  }
266
- if (afterAll.length > 0) {
267
- messages.push(...afterAll.map((item) => item.message))
268
- for (const item of afterAll) markGroup(item.group)
269
- changed = true
270
- }
271
-
272
- return changed ? { ...decision, messages } : decision
273
- } catch (error) {
274
- warnOnce(`${name}: prompt config failed, skipping: ${String((error && error.message) || error)}`)
275
- return decision
423
+ registeredCoordinator = coordinator
424
+ }
425
+ if (coordinator !== undefined) {
426
+ // 管理路径:先登记本来源,再交出唯一执行权(本监听器不再注入)。
427
+ if (registration === null) registration = coordinator.registerPreset(ctx, sourceId, source) ?? null
428
+ return next()
276
429
  }
430
+ const decision = await next()
431
+ if (!active) return decision
432
+ return runPreStepBatch({
433
+ ctx,
434
+ agent: payload?.agent,
435
+ decision,
436
+ configs: effectiveList,
437
+ promotion,
438
+ memo: injectedMemo,
439
+ warnOnce,
440
+ })
277
441
  }, { prepend })
442
+ // 释放边界:先摘监听器,再撤层级注册,最后丢弃去重快路径(顺序与 keepDisposer 内的 release 一致)。
443
+ return () => {
444
+ release()
445
+ try {
446
+ preStepDisposer()
447
+ } catch {
448
+ // 宿主已释放该 fiber 时监听器可能已不在:忽略。
449
+ }
450
+ try {
451
+ observerDisposer()
452
+ } catch {
453
+ // 同上。
454
+ }
455
+ releaseLayers()
456
+ }
278
457
  }