dsh-plugin-prompt-tool 0.8.1 → 2.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 (139) hide show
  1. package/README.md +84 -67
  2. package/engine/actions/append-context.mjs +49 -0
  3. package/engine/actions/assembly.mjs +150 -0
  4. package/engine/actions/catalog.mjs +92 -0
  5. package/engine/actions/content.mjs +205 -0
  6. package/engine/actions/decision.mjs +54 -0
  7. package/engine/actions/examples.mjs +11 -0
  8. package/engine/actions/guard.mjs +80 -0
  9. package/engine/actions/inbox-prepend.mjs +37 -0
  10. package/engine/actions/index.mjs +100 -0
  11. package/engine/actions/inject-text.mjs +22 -0
  12. package/engine/actions/pre-step-filter.mjs +55 -0
  13. package/engine/actions/registration.mjs +168 -0
  14. package/engine/actions/request-params.mjs +32 -0
  15. package/engine/actions/sdk-strip.mjs +30 -0
  16. package/engine/actions/shared.mjs +71 -0
  17. package/engine/actions.mjs +2 -931
  18. package/engine/branch.mjs +44 -0
  19. package/engine/character-tools.mjs +3 -6
  20. package/engine/compaction-epoch.mjs +31 -13
  21. package/engine/compositions/library/agent-instructions.yml +1 -1
  22. package/engine/compositions/library/command-goal.yml +1 -1
  23. package/engine/compositions/library/compaction.yml +1 -1
  24. package/engine/compositions/library/delegation-ptc.yml +1 -1
  25. package/engine/compositions/library/delegation.yml +1 -1
  26. package/engine/compositions/library/persistent-shell.yml +1 -1
  27. package/engine/compositions/library/planning.yml +1 -1
  28. package/engine/compositions/library/skill-filesystem-cordis.yml +1 -1
  29. package/engine/compositions/library/skill-filesystem.yml +1 -1
  30. package/engine/compositions/library/tool-ask-user.yml +1 -1
  31. package/engine/compositions/library/tool-bash.yml +1 -1
  32. package/engine/compositions/library/tool-cordis.yml +1 -1
  33. package/engine/compositions/library/tool-fs-search.yml +1 -1
  34. package/engine/compositions/library/tool-fs.yml +1 -1
  35. package/engine/compositions/library/tool-goal.yml +1 -1
  36. package/engine/compositions/library/tool-jobs.yml +1 -1
  37. package/engine/compositions/library/tool-plugin-manager-disabled.yml +1 -1
  38. package/engine/compositions/library/tool-plugin-manager.yml +1 -1
  39. package/engine/compositions/library/tool-present.yml +1 -1
  40. package/engine/compositions/library/tool-presentation.yml +1 -1
  41. package/engine/compositions/library/tool-pwsh.yml +1 -1
  42. package/engine/compositions/library/tool-skill.yml +1 -1
  43. package/engine/compositions/library/tool-todo.yml +1 -1
  44. package/engine/compositions/library/tool-web.yml +1 -1
  45. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  46. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  47. package/engine/compositions/source/local/rule-engine.yml +6 -0
  48. package/engine/condition.mjs +16 -3
  49. package/engine/conditions/anchor.mjs +32 -0
  50. package/engine/conditions/availability.mjs +47 -0
  51. package/engine/conditions/composite.mjs +83 -0
  52. package/engine/conditions/count.mjs +140 -0
  53. package/engine/conditions/evaluation.mjs +41 -0
  54. package/engine/conditions/examples.mjs +11 -0
  55. package/engine/conditions/index.mjs +85 -0
  56. package/engine/conditions/names.mjs +21 -0
  57. package/engine/conditions/phase.mjs +54 -0
  58. package/engine/conditions/preset.mjs +94 -0
  59. package/engine/conditions/scope.mjs +18 -0
  60. package/engine/conditions/session.mjs +22 -0
  61. package/engine/conditions/source.mjs +63 -0
  62. package/engine/conditions/subject.mjs +121 -0
  63. package/engine/conditions/text.mjs +44 -0
  64. package/engine/conditions/values.mjs +32 -0
  65. package/engine/dev-tool-search.mjs +213 -0
  66. package/engine/executor.mjs +93 -31
  67. package/engine/fillers.mjs +23 -16
  68. package/engine/instruction-hint.mjs +39 -23
  69. package/engine/layers.mjs +171 -77
  70. package/engine/order.mjs +20 -0
  71. package/engine/predicates.mjs +2 -753
  72. package/engine/rule-engine.mjs +40 -0
  73. package/engine/rule-runtime.mjs +110 -0
  74. package/engine/rule-spec.mjs +187 -0
  75. package/engine/schema.mjs +32 -11
  76. package/engine/session-var-tools.mjs +3 -6
  77. package/engine/shared.mjs +1 -1
  78. package/engine/st-render.mjs +8 -4
  79. package/engine/strategies.mjs +2 -235
  80. package/engine/subagent-tool-policy-core.mjs +7 -7
  81. package/engine/subagent-tool-policy.mjs +45 -20
  82. package/engine/tool-config-engine.mjs +53 -32
  83. package/engine/trigger-editor-meta.mjs +2 -21
  84. package/engine/trigger-spec.mjs +49 -133
  85. package/engine/world-book-tools.mjs +3 -6
  86. package/lib/client.js +10444 -10886
  87. package/lib/client.js.map +1 -1
  88. package/lib/index.d.mts +265 -361
  89. package/lib/index.mjs +1559 -2026
  90. package/lib/migrate-rules.d.mts +1 -0
  91. package/lib/migrate-rules.mjs +39 -0
  92. package/lib/module-package-CHPFweqK.mjs +2 -0
  93. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  94. package/lib/{paths-CY5psBqE.d.mts → paths-CyP0IkaM.d.mts} +388 -231
  95. package/lib/preset-transfer.d.mts +2 -2
  96. package/lib/preset-transfer.mjs +3 -2
  97. package/lib/{preset-package-BnJqrU4R.mjs → rules-migration-DJ6vASB5.mjs} +13825 -15283
  98. package/modules/ponytail/module.yml +406 -0
  99. package/modules/skill-surface/module.yml +74 -0
  100. package/modules/tool-surface/module.yml +67 -0
  101. package/package.json +50 -80
  102. package/templates/10-pre-step.yml +26 -22
  103. package/templates/14-first-turn-anchor.yml +25 -21
  104. package/templates/15-guide-auto.yml +34 -19
  105. package/templates/16-custom-fallback.yml +27 -15
  106. package/templates/18-placeholder-env-facts.yml +23 -14
  107. package/templates/20-system-section.yml +20 -11
  108. package/templates/30-runtime-context.yml +13 -7
  109. package/templates/40-agent-request.yml +11 -8
  110. package/templates/50-llm-stream.yml +14 -6
  111. package/templates/60-tool-pipeline.yml +15 -9
  112. package/templates/65-turn-stop.yml +19 -12
  113. package/templates/66-subagent-start.yml +12 -7
  114. package/templates/70-subagent-maintenance.yml +35 -29
  115. package/templates/75-subagent-end.yml +10 -5
  116. package/templates/80-tool-surface.yml +66 -0
  117. package/templates/policies/legacy-defaults.yml +34 -0
  118. package/templates/tools/50-delegate-worldbook.yml +1 -1
  119. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  120. package/engine/prompt-config-engine.mjs +0 -104
  121. package/preset/pt-cordis/preset.yml +0 -43
  122. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +0 -29
  123. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +0 -501
  124. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +0 -38
  125. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +0 -60
  126. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +0 -5
  127. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +0 -13
  128. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +0 -5
  129. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +0 -22
  130. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +0 -3
  131. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +0 -2
  132. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +0 -15
  133. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +0 -8
  134. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +0 -7
  135. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +0 -84
  136. package/preset/pt-custom/preset.yml +0 -15
  137. package/preset/pt-minimal/preset.yml +0 -27
  138. package/preset/pt-ptc/preset.yml +0 -45
  139. package/preset/pt-standard/preset.yml +0 -43
package/engine/layers.mjs CHANGED
@@ -18,6 +18,8 @@ import { KNOWN_STRATEGIES } from './schema.mjs'
18
18
  import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactValue } from './interpolate.mjs'
19
19
  import { getSessionVar, sessionVarsSnapshot } from './session-vars.mjs'
20
20
  import { conditionHit, lastAssistantText, subagentTextOf, toolArgsText } from './condition.mjs'
21
+ import { compareConfigSequence, compareTextPlacement } from './order.mjs'
22
+ import { ruleFrame, ruleMatches } from './conditions/evaluation.mjs'
21
23
 
22
24
  const name = 'prompt-config-engine'
23
25
  // 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
@@ -66,7 +68,7 @@ function officialAliasOf(name) {
66
68
  * 非法官方名(中文/大写/连字符)改写成确定性别名并改写文本引用;注册失败的名字退回静态解析。
67
69
  * @returns 各配置独立的引用白名单与别名;同一作用域内等价绑定只注册一次。
68
70
  */
69
- function registerOfficialVariables(ctx, configs, warnOnce) {
71
+ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
70
72
  const registries = new Map(configs.map((config) => [config, { protect: new Set(), alias: new Map(), registered: new Set() }]))
71
73
  const systemPrompt = getService(ctx, 'systemPrompt')
72
74
  if (systemPrompt === undefined || typeof systemPrompt.variable !== 'function') return registries
@@ -109,7 +111,7 @@ function registerOfficialVariables(ctx, configs, warnOnce) {
109
111
  usedNames.add(official)
110
112
  const source = isFact ? name.toLowerCase() : name
111
113
  const declaredValue = declared ? String(config.variables[name] ?? '') : undefined
112
- keepDisposer(ctx, systemPrompt.variable(official, (context) => {
114
+ keep(systemPrompt.variable(official, (context) => {
113
115
  const session = context?.agent?.session
114
116
  const override = getSessionVar(session, source)
115
117
  const value = override !== undefined ? String(override) : declaredValue ?? runtimeFactValue(source, session) ?? ''
@@ -160,30 +162,33 @@ function officialChannelText(text, label, registry, warnOnce) {
160
162
  return result.text
161
163
  }
162
164
 
163
- /** 文本型层分组:merged 模式按位置分组,否则每条独立。 */
165
+ /** 只合并排序后同来源、同官方档位和位置的连续 merged 卡,不跨过穿插段。 */
164
166
  function textLayerGroups(configs) {
165
- const sorted = [...configs].sort((a, b) => a.order - b.order)
167
+ const sorted = [...configs].sort(compareTextPlacement)
166
168
  const groups = []
167
- const index = new Map()
168
169
  for (const config of sorted) {
169
- const key = config.mergeMode === 'merged' ? `merged:${config.position ?? ''}` : undefined
170
- if (key === undefined) {
171
- groups.push([config])
172
- continue
173
- }
174
- let at = index.get(key)
175
- if (at === undefined) {
176
- at = groups.length
177
- index.set(key, at)
178
- groups.push([])
179
- }
180
- groups[at].push(config)
170
+ const previous = groups.at(-1)
171
+ const base = previous?.[0]
172
+ if (config.mergeMode === 'merged' && base?.mergeMode === 'merged'
173
+ && config.sourceModuleId === base.sourceModuleId
174
+ && config.position === base.position && config.order === base.order) previous.push(config)
175
+ else groups.push([config])
181
176
  }
182
177
  return groups
183
178
  }
184
179
 
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
+
185
190
  /** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
186
- function wireSystemSections(ctx, configs, registry, warnOnce) {
191
+ function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
187
192
  const disposers = []
188
193
  const systemPrompt = getService(ctx, 'systemPrompt')
189
194
  if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
@@ -206,15 +211,15 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
206
211
  .filter((item) => item.length > 0)
207
212
  .join('\n\n')
208
213
  : groupText
209
- keepDisposer(ctx, systemPrompt.section({
210
- name: typeof base.params?.sectionName === 'string' && base.params.sectionName.length > 0 ? base.params.sectionName : base.id,
214
+ keep(systemPrompt.section({
215
+ name: textRegistrationName(base, 'sectionName'),
211
216
  order: base.order,
212
217
  text,
213
218
  ...(base.params?.complete === true ? { complete: true } : {}),
214
219
  }), `${name}: section ${base.id}`)
215
220
  // suppressRuntimeContext 抑制该 scope 的动态 runtime-context 快照(多段重复调用幂等)。
216
221
  if (base.params?.suppressRuntimeContext === true) {
217
- keepDisposer(ctx, systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
222
+ keep(systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
218
223
  }
219
224
  } catch (error) {
220
225
  warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
@@ -223,8 +228,60 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
223
228
  return disposers
224
229
  }
225
230
 
231
+ async function resolvedContextText(ctx, config, context, registry, warnOnce) {
232
+ const agent = context.agent
233
+ const session = agent?.session
234
+ const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
235
+ 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) : ''
239
+ return officialChannelText(rendered, `runtime-context ${config.id}`, registry, warnOnce)
240
+ }
241
+
242
+ /** 条件化文本保留官方排序占位;仅在本次真实 assembly 填充,不缓存 context 身份。 */
243
+ function wireRuleTextContributions(ctx, configs, registry, warnOnce, keep, on) {
244
+ if (!configs.length) return []
245
+ const systemPrompt = getService(ctx, 'systemPrompt')
246
+ if (systemPrompt === undefined) { warnOnce(`${name}: systemPrompt unavailable`); return [] }
247
+ const variable = `pt_rule_${newMessageId('text').replace(/[^a-z0-9_]/gi, '_').toLowerCase()}`
248
+ const marker = `{{${variable}}}`
249
+ keep(systemPrompt.variable(variable, () => ''), `${name}: conditional text marker`)
250
+ const bindings = []
251
+ for (const layer of ['system-section', 'runtime-context']) for (const group of textLayerGroups(configs.filter(config => config.layer === layer))) {
252
+ const section = layer === 'system-section'
253
+ const base = group[0]
254
+ const entryName = textRegistrationName(base, section ? 'sectionName' : 'contextName')
255
+ keep(systemPrompt[section ? 'section' : 'context']({ name: entryName, order: base.order, text: marker }), `${name}: conditional ${entryName}`)
256
+ bindings.push({ group, name: entryName, collection: section ? 'sections' : 'contexts' })
257
+ }
258
+ let active = true
259
+ keep(() => { active = false }, `${name}: conditional text lifecycle`)
260
+ return [on('system-prompt/assemble', async (assembly, context, next, invocation) => {
261
+ const frame = invocation ?? ruleFrame('system-prompt/assemble', [assembly, context], warnOnce)
262
+ for (const binding of bindings) {
263
+ const entry = assembly[binding.collection].find(item => item.name === binding.name && item.text === marker)
264
+ if (entry === undefined) continue
265
+ entry.text = ''
266
+ const blocks = []
267
+ for (const config of binding.group) {
268
+ if (!active || context.signal?.aborted || !ruleMatches(config.rule, frame) || !matchesAgentScope(config, context.agent)) continue
269
+ try {
270
+ const text = config.layer === 'runtime-context' && (config.strategy === 'placeholder' || !KNOWN_STRATEGIES.has(config.strategy))
271
+ ? await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
272
+ : configText(config, registry, warnOnce, context)
273
+ if (text.length) blocks.push(text)
274
+ } catch (error) { warnOnce(`${name}: rule text ${config.id} failed: ${String(error?.message ?? error)}`) }
275
+ }
276
+ if (active && !context.signal?.aborted) entry.text = blocks.join('\n\n')
277
+ }
278
+ return next()
279
+ })]
280
+
281
+ }
282
+
226
283
  /** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
227
- function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
284
+ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
228
285
  const disposers = []
229
286
  const systemPrompt = getService(ctx, 'systemPrompt')
230
287
  if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
@@ -243,8 +300,8 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
243
300
  const render = context => group.map(config => configText(config, registry, warnOnce, context)).filter(item => item.length > 0).join('\n\n')
244
301
  const text = dynamic ? render : render()
245
302
  if (!dynamic && text.length === 0) continue
246
- keepDisposer(ctx, systemPrompt.context({
247
- name: typeof base.params?.contextName === 'string' && base.params.contextName.length > 0 ? base.params.contextName : base.id,
303
+ keep(systemPrompt.context({
304
+ name: textRegistrationName(base, 'contextName'),
248
305
  order: base.order,
249
306
  text,
250
307
  }), `${name}: context ${base.id}`)
@@ -255,18 +312,18 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
255
312
  // 官方 provider 必须同步:先注册可渲染为空的私有变量占位,再在 waterfall 中填充。
256
313
  // 标记随官方排序/遮蔽进入本次 assembly,无会话缓存,也不依赖调用方 context 的对象身份。
257
314
  const placeholders = configs.filter(needsResolver)
258
- .sort((a, b) => a.order - b.order)
315
+ .sort(compareTextPlacement)
259
316
  if (placeholders.length === 0) return disposers
260
317
  const slotVariable = `pt_runtime_${newMessageId('context').replace(/[^a-z0-9_]/gi, '_').toLowerCase()}`
261
318
  const slotText = `{{${slotVariable}}}`
262
319
  const registered = new Map()
263
320
  let active = true
264
- keepDisposer(ctx, () => { active = false }, `${name}: runtime-context lifecycle`)
265
- keepDisposer(ctx, systemPrompt.variable(slotVariable, () => ''), `${name}: runtime-context placeholder`)
321
+ keep(() => { active = false }, `${name}: runtime-context lifecycle`)
322
+ keep(systemPrompt.variable(slotVariable, () => ''), `${name}: runtime-context placeholder`)
266
323
  for (const config of placeholders) {
267
324
  try {
268
- const contextName = typeof config.params?.contextName === 'string' && config.params.contextName.length > 0 ? config.params.contextName : config.id
269
- keepDisposer(ctx, systemPrompt.context({
325
+ const contextName = textRegistrationName(config, 'contextName')
326
+ keep(systemPrompt.context({
270
327
  name: contextName,
271
328
  order: config.order,
272
329
  text: slotText,
@@ -276,22 +333,15 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
276
333
  warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
277
334
  }
278
335
  }
279
- disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
336
+ disposers.push(on('system-prompt/assemble', async (assembly, context, next) => {
280
337
  const entries = assembly.contexts.filter(entry => registered.has(entry.name) && entry.text === slotText)
281
338
  for (const entry of entries) {
282
339
  const config = registered.get(entry.name)
283
340
  entry.text = ''
284
341
  if (!active || context.signal?.aborted) continue
285
342
  try {
286
- const agent = context.agent
287
- const session = agent?.session
288
- const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
289
- if (!active || context.signal?.aborted || resolved === null || resolved === undefined) continue
290
- const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
291
- const rendered = config.texts.length > 0
292
- ? interpolateVariables(config.texts.join('\n\n'), variables, session)
293
- : typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
294
- entry.text = officialChannelText(rendered, `runtime-context ${config.id}`, registry.get(config), warnOnce)
343
+ const text = await resolvedContextText(ctx, config, context, registry.get(config), warnOnce)
344
+ if (active && !context.signal?.aborted) entry.text = text
295
345
  } catch (error) {
296
346
  warnOnce(`${name}: runtime-context ${config.id} resolve failed: ${String(error?.message ?? error)}`)
297
347
  }
@@ -336,21 +386,22 @@ export function applyAgentRequestParams(params, base) {
336
386
  }
337
387
 
338
388
  /** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
339
- function wireAgentRequests(ctx, configs, warnOnce) {
340
- const disposers = []
341
- for (const config of configs) {
342
- disposers.push(ctx.on('agent/request', async (payload, next) => {
343
- const base = await next()
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) {
344
397
  try {
345
- if (!matchesAgentScope(config, payload?.agent)) return base
346
- return applyAgentRequestParams(config.params, base)
398
+ if (ruleMatches(config.rule, frame) && matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
347
399
  } catch (error) {
348
400
  warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
349
- return base
350
401
  }
351
- }))
352
- }
353
- return disposers
402
+ }
403
+ return result
404
+ })]
354
405
  }
355
406
 
356
407
  /** 把流替换为提示词配置文本的最小合法 chunk 序列。 */
@@ -361,11 +412,15 @@ async function* replacedStream(text) {
361
412
  }
362
413
 
363
414
  /** llm-stream:pass 透传;replace 用提示词配置文本替代整个模型流。 */
364
- function wireLlmStreams(ctx, configs, warnOnce) {
415
+ function wireLlmStreams(ctx, configs, warnOnce, on) {
365
416
  const disposers = []
366
417
  for (const config of configs) {
367
- disposers.push(ctx.on('llm/stream', (options, next) => {
418
+ disposers.push(on('llm/stream', (options, next, invocation) => {
368
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()
369
424
  const mode = config.params?.mode ?? 'pass'
370
425
  if (mode === 'replace' && config.texts.length > 0 && matchesModel(config.modelScope, options?.model)) {
371
426
  return replacedStream(config.texts.join('\n\n'))
@@ -382,13 +437,21 @@ function wireLlmStreams(ctx, configs, warnOnce) {
382
437
 
383
438
  /** 条件判定的匹配器与取文本逻辑由 condition.mjs 承载(pre-step 与其他层共用)。 */
384
439
 
385
- /** 插件来源的 user 消息:与 anchor-turn / progress-reminder 同一形状。 */
440
+ /**
441
+ * 插件来源的 user 消息:与 anchor-turn / progress-reminder 同一形状。
442
+ *
443
+ * `kind` 必须是**生产者名**而不是裸 `plugin`:会话格式 v4 把 `{ kind: 'plugin', plugin }`
444
+ * 列为退役包装并直接拒绝(`dsh-session-format-v3-to-v4` 的 `assertV4MessageSources` /
445
+ * `assertV4SourceRowAdmission` 一律抛 `format v4 message requires a producer-owned source kind`),
446
+ * 迁移只在读旧日志时把 `plugin` 折进 kind。写 `kind: 'plugin'` 会让本次 splice 被 codec
447
+ * 拒绝、连 `agent/inbox/spliced` 都落不了盘——子代理启动因此整段失败。
448
+ */
386
449
  export function pluginMessage(prefix, text, summary) {
387
450
  return {
388
451
  id: newMessageId(prefix),
389
452
  role: 'user',
390
453
  content: [{ type: 'text', text }],
391
- source: { kind: 'plugin', plugin: name, form: 'notice', summary },
454
+ source: { kind: `plugin:${name}`, plugin: name, form: 'notice', summary },
392
455
  }
393
456
  }
394
457
 
@@ -406,13 +469,14 @@ function layerText(config, agent, warnOnce) {
406
469
  }
407
470
 
408
471
  /** tool-pipeline:pre-execute 判定、execute 包装、post-execute 结果替换/阻断。 */
409
- function wireToolPipelines(ctx, configs, warnOnce) {
472
+ function wireToolPipelines(ctx, configs, warnOnce, on) {
410
473
  const disposers = []
411
474
  for (const config of configs) {
412
475
  const names = parseToolNames(config.params?.toolNames)
413
476
  const matchesTool = (exec) => names.length === 0 || names.includes(exec?.name)
414
- disposers.push(ctx.on('tools/pre-execute', async (exec, next) => {
477
+ disposers.push(on('tools/pre-execute', async (exec, next, invocation) => {
415
478
  try {
479
+ if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/pre-execute', [exec], warnOnce))) return next()
416
480
  if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
417
481
  if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments) })) return next()
418
482
  const decision = config.params?.preDecision ?? 'allow'
@@ -427,8 +491,9 @@ function wireToolPipelines(ctx, configs, warnOnce) {
427
491
  return next()
428
492
  }
429
493
  }))
430
- disposers.push(ctx.on('tools/post-execute', async (exec, result, next) => {
494
+ disposers.push(on('tools/post-execute', async (exec, result, next, invocation) => {
431
495
  try {
496
+ if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/post-execute', [exec, result], warnOnce))) return next()
432
497
  if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
433
498
  if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments), resultText: extractText(result) })) return next()
434
499
  const action = config.params?.postAction ?? 'accept'
@@ -515,14 +580,19 @@ export function createTurnStopBudget() {
515
580
  }
516
581
 
517
582
  /** turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。 */
518
- function wireTurnStops(ctx, configs, warnOnce) {
583
+ function wireTurnStops(ctx, configs, warnOnce, on) {
519
584
  const disposers = []
520
585
  if (configs.length === 0) return disposers
521
- const budget = createTurnStopBudget()
586
+ const budgets = new Map()
522
587
 
523
588
  for (const config of configs) {
524
- disposers.push(ctx.on('agent/turn-stopping', ({ agent, turn } = {}) => {
589
+ const source = config.sourceModuleId ?? ''
590
+ if (!budgets.has(source)) budgets.set(source, createTurnStopBudget())
591
+ const budget = budgets.get(source)
592
+ disposers.push(on('agent/turn-stopping', (payload = {}, _next, invocation) => {
593
+ const { agent, turn } = payload
525
594
  try {
595
+ if (!ruleMatches(config.rule, invocation ?? ruleFrame('agent/turn-stopping', [payload], warnOnce))) return
526
596
  const session = agent?.session
527
597
  if (session?.id === undefined || typeof agent.steer !== 'function') return
528
598
  if (!matchesAgentScope(config, agent)) return
@@ -560,33 +630,39 @@ function mainSessionForChild(ctx, id) {
560
630
  }
561
631
 
562
632
  /** 子代理生命周期:启动注入子代理;结束默认观察,可显式给所属主会话投递上下文。 */
563
- function wireSubagentEvents(ctx, configs, warnOnce) {
633
+ function wireSubagentEvents(ctx, configs, warnOnce, on) {
564
634
  const disposers = []
565
635
  const startConfigs = configs.filter((config) => config.layer === 'subagent-start')
566
636
  const endConfigs = configs.filter((config) => config.layer === 'subagent-end')
567
- const injectMain = endConfigs.some((config) => config.params?.action === 'inject-main')
568
637
  // B4 保留原状(不迁 `sessionState`):键是 `runId`(运行标识)而不是 session,不属于
569
638
  // 「按会话索引」的会话态,套统一接口会把语义挪到错误的键上;它的 `delete(最旧)` 也是与
570
639
  // `clear()` 不同的另一档淘汰策略,无等价证明前保留。
571
640
  const runs = new Map()
572
- if (injectMain) {
641
+ if (endConfigs.length > 0) {
573
642
  disposers.push(ctx.on('subagent/start', (info) => {
574
643
  if (typeof info?.runId !== 'string' || typeof info?.id !== 'string') return
644
+ const child = getService(ctx, 'agents')?.get?.(info.id)
645
+ const session = child?.session ?? getService(ctx, 'sessions')?.get?.(info.id)
646
+ if (session === undefined) return
575
647
  const mainId = mainSessionForChild(ctx, info.id)
576
- if (mainId === undefined) return
577
648
  // ponytail: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
578
649
  if (runs.size >= MAX_TRACKED_SESSIONS) runs.delete(runs.keys().next().value)
579
- runs.set(info.runId, { id: info.id, mainId, model: getService(ctx, 'agents')?.get?.(info.id)?.options?.model })
650
+ runs.set(info.runId, { id: info.id, mainId, agent: child, session, model: child?.options?.model })
580
651
  }))
581
- ctx.effect?.(() => () => runs.clear())
652
+ const clearRuns = () => runs.clear()
653
+ disposers.push(clearRuns)
654
+ ctx.effect?.(() => clearRuns)
582
655
  }
583
656
  if (startConfigs.length > 0) {
584
- disposers.push(ctx.on('subagent/start', (info) => {
657
+ disposers.push(on('subagent/start', (info, _next, invocation) => {
585
658
  try {
586
659
  const child = getService(ctx, 'agents')?.get?.(info?.id)
587
660
  if (child === undefined) return
661
+ const frame = invocation ?? ruleFrame('subagent/start', [info], warnOnce)
662
+ frame.subject.agent = child
588
663
  const subagentText = subagentTextOf(info)
589
664
  for (const config of startConfigs) {
665
+ if (!ruleMatches(config.rule, frame)) continue
590
666
  if (!matchesAgentScope(config, child)) continue
591
667
  if (!conditionHit(config, { subagentText })) continue
592
668
  if (typeof child.inject !== 'function') continue
@@ -600,14 +676,19 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
600
676
  }))
601
677
  }
602
678
  if (endConfigs.length > 0) {
603
- disposers.push(ctx.on('subagent/end', (info) => {
679
+ disposers.push(on('subagent/end', (info, _next, invocation) => {
604
680
  try {
605
681
  const child = getService(ctx, 'agents')?.get?.(info?.id)
606
682
  const recorded = runs.get(info?.runId)
607
683
  runs.delete(info?.runId)
608
684
  if (recorded !== undefined && recorded.id !== info?.id) return
609
685
  const subagentText = subagentTextOf(info)
686
+ const frame = invocation ?? ruleFrame('subagent/end', [info], warnOnce)
687
+ frame.subject.agent = child ?? recorded?.agent
688
+ frame.subject.session = child?.session ?? recorded?.session ?? getService(ctx, 'sessions')?.get?.(info?.id)
689
+ frame.subject.model = child?.options?.model ?? recorded?.model
610
690
  for (const config of endConfigs) {
691
+ if (!ruleMatches(config.rule, frame)) continue
611
692
  if (!matchesModel(config.modelScope, child?.options?.model ?? recorded?.model)) continue
612
693
  if (!conditionHit(config, { subagentText })) continue
613
694
  if (config.params?.action !== 'inject-main') {
@@ -645,17 +726,30 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
645
726
  * @returns 聚合 disposer:回收本次接线显式创建的 waterfall 监听器;段/上下文/
646
727
  * 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
647
728
  */
648
- export function wireLayers(ctx, configs, warnOnce) {
729
+ export function wireLayers(ctx, configs, warnOnce, options = {}) {
730
+ configs = [...configs].sort(compareConfigSequence)
731
+ const owned = []
732
+ const keep = (dispose, label) => {
733
+ if (typeof dispose !== 'function') return
734
+ let active = true
735
+ const release = () => { if (active) { active = false; dispose() } }
736
+ owned.push(release); keepDisposer(ctx, release, label)
737
+ }
738
+ const on = options.on ?? ((...args) => ctx.on(...args))
649
739
  // 官方插值两层共享一份变量注册:运行时事实按 assembly 求值,非法名走别名改写。
650
- const registry = registerOfficialVariables(ctx, configs.filter((config) => config.layer === 'system-section' || config.layer === 'runtime-context'), warnOnce)
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))
651
743
  const registered = [
652
- wireSystemSections(ctx, configs.filter((config) => config.layer === 'system-section'), registry, warnOnce),
653
- wireRuntimeContexts(ctx, configs.filter((config) => config.layer === 'runtime-context'), registry, warnOnce),
654
- wireAgentRequests(ctx, configs.filter((config) => config.layer === 'agent-request'), warnOnce),
655
- wireLlmStreams(ctx, configs.filter((config) => config.layer === 'llm-stream'), warnOnce),
656
- wireToolPipelines(ctx, configs.filter((config) => config.layer === 'tool-pipeline'), warnOnce),
657
- wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce),
658
- wireSubagentEvents(ctx, configs.filter((config) => config.layer === 'subagent-start' || config.layer === 'subagent-end'), warnOnce),
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,
659
753
  ].flat().filter((disposer) => typeof disposer === 'function')
660
754
  return () => {
661
755
  for (const dispose of registered.splice(0)) {
@@ -0,0 +1,20 @@
1
+ /** 加载器私有元数据:不进入 YAML、公共配置或客户端载荷。 */
2
+ export const FILE_SEQUENCE = Symbol('prompt-tool:file-sequence')
3
+
4
+ const compareText = (a, b) => a < b ? -1 : a > b ? 1 : 0
5
+
6
+ /** 模块配置按持久序号执行;无物化来源的独立调用保留原 order 语义。 */
7
+ export function compareConfigSequence(a, b) {
8
+ const difference = (a.sequence ?? a.order ?? 0) - (b.sequence ?? b.order ?? 0)
9
+ if (difference !== 0) return difference
10
+ if (a.sourceModuleId === undefined && b.sourceModuleId === undefined) return 0
11
+ return compareText(a.sourceModuleId ?? '', b.sourceModuleId ?? '')
12
+ || compareText(a.ruleId ?? a.id, b.ruleId ?? b.id)
13
+ || (a.ruleActionIndex ?? 0) - (b.ruleActionIndex ?? 0)
14
+ || compareText(a.id, b.id)
15
+ }
16
+
17
+ /** 官方文本段的定位仍由 order 决定;序号只提供同位置的稳定顺序。 */
18
+ export function compareTextPlacement(a, b) {
19
+ return a.order - b.order || compareConfigSequence(a, b)
20
+ }