dsh-plugin-prompt-tool 1.0.0 → 2.0.1

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 (95) hide show
  1. package/README.md +38 -29
  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/compaction-epoch.mjs +31 -13
  20. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  21. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  22. package/engine/compositions/source/local/rule-engine.yml +6 -0
  23. package/engine/condition.mjs +16 -3
  24. package/engine/conditions/anchor.mjs +32 -0
  25. package/engine/conditions/availability.mjs +47 -0
  26. package/engine/conditions/composite.mjs +83 -0
  27. package/engine/conditions/count.mjs +140 -0
  28. package/engine/conditions/evaluation.mjs +41 -0
  29. package/engine/conditions/examples.mjs +11 -0
  30. package/engine/conditions/index.mjs +85 -0
  31. package/engine/conditions/names.mjs +21 -0
  32. package/engine/conditions/phase.mjs +54 -0
  33. package/engine/conditions/preset.mjs +94 -0
  34. package/engine/conditions/scope.mjs +18 -0
  35. package/engine/conditions/session.mjs +22 -0
  36. package/engine/conditions/source.mjs +63 -0
  37. package/engine/conditions/subject.mjs +121 -0
  38. package/engine/conditions/text.mjs +44 -0
  39. package/engine/conditions/values.mjs +32 -0
  40. package/engine/dev-tool-search.mjs +213 -0
  41. package/engine/executor.mjs +55 -6
  42. package/engine/fillers.mjs +23 -16
  43. package/engine/instruction-hint.mjs +34 -22
  44. package/engine/layers.mjs +135 -49
  45. package/engine/order.mjs +4 -1
  46. package/engine/predicates.mjs +2 -753
  47. package/engine/rule-engine.mjs +40 -0
  48. package/engine/rule-runtime.mjs +110 -0
  49. package/engine/rule-spec.mjs +187 -0
  50. package/engine/schema.mjs +16 -5
  51. package/engine/shared.mjs +1 -1
  52. package/engine/strategies.mjs +2 -235
  53. package/engine/subagent-tool-policy-core.mjs +7 -7
  54. package/engine/subagent-tool-policy.mjs +20 -10
  55. package/engine/tool-config-engine.mjs +53 -32
  56. package/engine/trigger-spec.mjs +49 -133
  57. package/lib/client.js +11117 -10860
  58. package/lib/client.js.map +1 -1
  59. package/lib/index.d.mts +82 -139
  60. package/lib/index.mjs +826 -1087
  61. package/lib/migrate-rules.d.mts +1 -0
  62. package/lib/migrate-rules.mjs +39 -0
  63. package/lib/module-package-CHPFweqK.mjs +2 -0
  64. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  65. package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
  66. package/lib/preset-transfer.d.mts +2 -2
  67. package/lib/preset-transfer.mjs +3 -2
  68. package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
  69. package/modules/ponytail/module.yml +381 -147
  70. package/modules/skill-surface/module.yml +74 -0
  71. package/modules/tool-surface/module.yml +67 -0
  72. package/package.json +5 -1
  73. package/templates/10-pre-step.yml +26 -22
  74. package/templates/14-first-turn-anchor.yml +25 -21
  75. package/templates/15-guide-auto.yml +34 -19
  76. package/templates/16-custom-fallback.yml +27 -15
  77. package/templates/18-placeholder-env-facts.yml +23 -14
  78. package/templates/20-system-section.yml +16 -10
  79. package/templates/30-runtime-context.yml +13 -7
  80. package/templates/40-agent-request.yml +11 -8
  81. package/templates/50-llm-stream.yml +14 -6
  82. package/templates/60-tool-pipeline.yml +15 -9
  83. package/templates/65-turn-stop.yml +19 -12
  84. package/templates/66-subagent-start.yml +12 -7
  85. package/templates/70-subagent-maintenance.yml +35 -29
  86. package/templates/75-subagent-end.yml +10 -5
  87. package/templates/80-tool-surface.yml +66 -0
  88. package/templates/policies/legacy-defaults.yml +34 -0
  89. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  90. package/engine/prompt-config-engine.mjs +0 -104
  91. package/engine/trigger-editor-meta.mjs +0 -48
  92. package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
  93. package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
  94. package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
  95. package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
package/engine/layers.mjs CHANGED
@@ -19,6 +19,7 @@ import { interpolateVariables, stripUnresolvedRefs, RUNTIME_FACTS, runtimeFactVa
19
19
  import { getSessionVar, sessionVarsSnapshot } from './session-vars.mjs'
20
20
  import { conditionHit, lastAssistantText, subagentTextOf, toolArgsText } from './condition.mjs'
21
21
  import { compareConfigSequence, compareTextPlacement } from './order.mjs'
22
+ import { ruleFrame, ruleMatches } from './conditions/evaluation.mjs'
22
23
 
23
24
  const name = 'prompt-config-engine'
24
25
  // 同一宿主会话共享投递账本,避免预设作用域重挂后重投同一次结束通知。
@@ -67,7 +68,7 @@ function officialAliasOf(name) {
67
68
  * 非法官方名(中文/大写/连字符)改写成确定性别名并改写文本引用;注册失败的名字退回静态解析。
68
69
  * @returns 各配置独立的引用白名单与别名;同一作用域内等价绑定只注册一次。
69
70
  */
70
- function registerOfficialVariables(ctx, configs, warnOnce) {
71
+ function registerOfficialVariables(ctx, configs, warnOnce, keep) {
71
72
  const registries = new Map(configs.map((config) => [config, { protect: new Set(), alias: new Map(), registered: new Set() }]))
72
73
  const systemPrompt = getService(ctx, 'systemPrompt')
73
74
  if (systemPrompt === undefined || typeof systemPrompt.variable !== 'function') return registries
@@ -110,7 +111,7 @@ function registerOfficialVariables(ctx, configs, warnOnce) {
110
111
  usedNames.add(official)
111
112
  const source = isFact ? name.toLowerCase() : name
112
113
  const declaredValue = declared ? String(config.variables[name] ?? '') : undefined
113
- keepDisposer(ctx, systemPrompt.variable(official, (context) => {
114
+ keep(systemPrompt.variable(official, (context) => {
114
115
  const session = context?.agent?.session
115
116
  const override = getSessionVar(session, source)
116
117
  const value = override !== undefined ? String(override) : declaredValue ?? runtimeFactValue(source, session) ?? ''
@@ -187,7 +188,7 @@ function textRegistrationName(config, field) {
187
188
  }
188
189
 
189
190
  /** system-section:注册静态 system prompt 段(支持官方 {{variable}} 渲染与 merged 拼接)。 */
190
- function wireSystemSections(ctx, configs, registry, warnOnce) {
191
+ function wireSystemSections(ctx, configs, registry, warnOnce, keep) {
191
192
  const disposers = []
192
193
  const systemPrompt = getService(ctx, 'systemPrompt')
193
194
  if (systemPrompt === undefined || typeof systemPrompt.section !== 'function') {
@@ -210,7 +211,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
210
211
  .filter((item) => item.length > 0)
211
212
  .join('\n\n')
212
213
  : groupText
213
- keepDisposer(ctx, systemPrompt.section({
214
+ keep(systemPrompt.section({
214
215
  name: textRegistrationName(base, 'sectionName'),
215
216
  order: base.order,
216
217
  text,
@@ -218,7 +219,7 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
218
219
  }), `${name}: section ${base.id}`)
219
220
  // suppressRuntimeContext 抑制该 scope 的动态 runtime-context 快照(多段重复调用幂等)。
220
221
  if (base.params?.suppressRuntimeContext === true) {
221
- keepDisposer(ctx, systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
222
+ keep(systemPrompt.suppressRuntimeContext(), `${name}: suppressRuntimeContext ${base.id}`)
222
223
  }
223
224
  } catch (error) {
224
225
  warnOnce(`${name}: system-section config ${base.id} failed: ${String(error?.message ?? error)}`)
@@ -227,8 +228,60 @@ function wireSystemSections(ctx, configs, registry, warnOnce) {
227
228
  return disposers
228
229
  }
229
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
+
230
283
  /** runtime-context:注册运行时上下文;静态文本支持 merged,动态策略在官方 assembly waterfall 中填充。 */
231
- function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
284
+ function wireRuntimeContexts(ctx, configs, registry, warnOnce, keep, on) {
232
285
  const disposers = []
233
286
  const systemPrompt = getService(ctx, 'systemPrompt')
234
287
  if (systemPrompt === undefined || typeof systemPrompt.context !== 'function') {
@@ -247,7 +300,7 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
247
300
  const render = context => group.map(config => configText(config, registry, warnOnce, context)).filter(item => item.length > 0).join('\n\n')
248
301
  const text = dynamic ? render : render()
249
302
  if (!dynamic && text.length === 0) continue
250
- keepDisposer(ctx, systemPrompt.context({
303
+ keep(systemPrompt.context({
251
304
  name: textRegistrationName(base, 'contextName'),
252
305
  order: base.order,
253
306
  text,
@@ -265,12 +318,12 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
265
318
  const slotText = `{{${slotVariable}}}`
266
319
  const registered = new Map()
267
320
  let active = true
268
- keepDisposer(ctx, () => { active = false }, `${name}: runtime-context lifecycle`)
269
- 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`)
270
323
  for (const config of placeholders) {
271
324
  try {
272
325
  const contextName = textRegistrationName(config, 'contextName')
273
- keepDisposer(ctx, systemPrompt.context({
326
+ keep(systemPrompt.context({
274
327
  name: contextName,
275
328
  order: config.order,
276
329
  text: slotText,
@@ -280,22 +333,15 @@ function wireRuntimeContexts(ctx, configs, registry, warnOnce) {
280
333
  warnOnce(`${name}: runtime-context placeholder ${config.id} failed: ${String(error?.message ?? error)}`)
281
334
  }
282
335
  }
283
- disposers.push(ctx.on('system-prompt/assemble', async (assembly, context, next) => {
336
+ disposers.push(on('system-prompt/assemble', async (assembly, context, next) => {
284
337
  const entries = assembly.contexts.filter(entry => registered.has(entry.name) && entry.text === slotText)
285
338
  for (const entry of entries) {
286
339
  const config = registered.get(entry.name)
287
340
  entry.text = ''
288
341
  if (!active || context.signal?.aborted) continue
289
342
  try {
290
- const agent = context.agent
291
- const session = agent?.session
292
- const resolved = await config.resolve({ ctx, agent, session, signal: context.signal, decision: { kind: 'ok', messages: [] }, messages: [] })
293
- if (!active || context.signal?.aborted || resolved === null || resolved === undefined) continue
294
- const variables = { ...config.variables, ...(resolved.variables !== null && typeof resolved.variables === 'object' ? resolved.variables : {}) }
295
- const rendered = config.texts.length > 0
296
- ? interpolateVariables(config.texts.join('\n\n'), variables, session)
297
- : typeof resolved.text === 'string' ? interpolateVariables(resolved.text, variables, session) : ''
298
- 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
299
345
  } catch (error) {
300
346
  warnOnce(`${name}: runtime-context ${config.id} resolve failed: ${String(error?.message ?? error)}`)
301
347
  }
@@ -340,15 +386,16 @@ export function applyAgentRequestParams(params, base) {
340
386
  }
341
387
 
342
388
  /** agent-request:对冻结的 LlmCallConfig 做浅合并 / 整体替换 / 按值条件删键。 */
343
- function wireAgentRequests(ctx, configs, warnOnce) {
389
+ function wireAgentRequests(ctx, configs, warnOnce, on) {
344
390
  if (configs.length === 0) return []
345
391
  const ordered = configs.some(config => config.sequence !== undefined) ? configs : [...configs].reverse()
346
- return [ctx.on('agent/request', async (payload, next) => {
392
+ return [on('agent/request', async (payload, next, invocation) => {
347
393
  let result = await next()
394
+ const frame = invocation ?? ruleFrame('agent/request', [payload], warnOnce)
348
395
  // 模块请求在 next 返回后按序合并;无文件来源的独立调用保持原回栈顺序。
349
396
  for (const config of ordered) {
350
397
  try {
351
- if (matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
398
+ if (ruleMatches(config.rule, frame) && matchesAgentScope(config, payload?.agent)) result = applyAgentRequestParams(config.params, result)
352
399
  } catch (error) {
353
400
  warnOnce(`${name}: agent-request config ${config.id} failed: ${String(error?.message ?? error)}`)
354
401
  }
@@ -365,11 +412,15 @@ async function* replacedStream(text) {
365
412
  }
366
413
 
367
414
  /** llm-stream:pass 透传;replace 用提示词配置文本替代整个模型流。 */
368
- function wireLlmStreams(ctx, configs, warnOnce) {
415
+ function wireLlmStreams(ctx, configs, warnOnce, on) {
369
416
  const disposers = []
370
417
  for (const config of configs) {
371
- disposers.push(ctx.on('llm/stream', (options, next) => {
418
+ disposers.push(on('llm/stream', (options, next, invocation) => {
372
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()
373
424
  const mode = config.params?.mode ?? 'pass'
374
425
  if (mode === 'replace' && config.texts.length > 0 && matchesModel(config.modelScope, options?.model)) {
375
426
  return replacedStream(config.texts.join('\n\n'))
@@ -386,13 +437,21 @@ function wireLlmStreams(ctx, configs, warnOnce) {
386
437
 
387
438
  /** 条件判定的匹配器与取文本逻辑由 condition.mjs 承载(pre-step 与其他层共用)。 */
388
439
 
389
- /** 插件来源的 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
+ */
390
449
  export function pluginMessage(prefix, text, summary) {
391
450
  return {
392
451
  id: newMessageId(prefix),
393
452
  role: 'user',
394
453
  content: [{ type: 'text', text }],
395
- source: { kind: 'plugin', plugin: name, form: 'notice', summary },
454
+ source: { kind: `plugin:${name}`, plugin: name, form: 'notice', summary },
396
455
  }
397
456
  }
398
457
 
@@ -410,13 +469,14 @@ function layerText(config, agent, warnOnce) {
410
469
  }
411
470
 
412
471
  /** tool-pipeline:pre-execute 判定、execute 包装、post-execute 结果替换/阻断。 */
413
- function wireToolPipelines(ctx, configs, warnOnce) {
472
+ function wireToolPipelines(ctx, configs, warnOnce, on) {
414
473
  const disposers = []
415
474
  for (const config of configs) {
416
475
  const names = parseToolNames(config.params?.toolNames)
417
476
  const matchesTool = (exec) => names.length === 0 || names.includes(exec?.name)
418
- disposers.push(ctx.on('tools/pre-execute', async (exec, next) => {
477
+ disposers.push(on('tools/pre-execute', async (exec, next, invocation) => {
419
478
  try {
479
+ if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/pre-execute', [exec], warnOnce))) return next()
420
480
  if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
421
481
  if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments) })) return next()
422
482
  const decision = config.params?.preDecision ?? 'allow'
@@ -431,8 +491,9 @@ function wireToolPipelines(ctx, configs, warnOnce) {
431
491
  return next()
432
492
  }
433
493
  }))
434
- disposers.push(ctx.on('tools/post-execute', async (exec, result, next) => {
494
+ disposers.push(on('tools/post-execute', async (exec, result, next, invocation) => {
435
495
  try {
496
+ if (!ruleMatches(config.rule, invocation ?? ruleFrame('tools/post-execute', [exec, result], warnOnce))) return next()
436
497
  if (!matchesTool(exec) || !matchesAgentScope(config, exec?.agent)) return next()
437
498
  if (!conditionHit(config, { argsText: toolArgsText(exec?.arguments), resultText: extractText(result) })) return next()
438
499
  const action = config.params?.postAction ?? 'accept'
@@ -519,7 +580,7 @@ export function createTurnStopBudget() {
519
580
  }
520
581
 
521
582
  /** turn-stop:命中条件时阻止本轮停止并强制续跑一步,上限在引擎内。 */
522
- function wireTurnStops(ctx, configs, warnOnce) {
583
+ function wireTurnStops(ctx, configs, warnOnce, on) {
523
584
  const disposers = []
524
585
  if (configs.length === 0) return disposers
525
586
  const budgets = new Map()
@@ -528,8 +589,10 @@ function wireTurnStops(ctx, configs, warnOnce) {
528
589
  const source = config.sourceModuleId ?? ''
529
590
  if (!budgets.has(source)) budgets.set(source, createTurnStopBudget())
530
591
  const budget = budgets.get(source)
531
- disposers.push(ctx.on('agent/turn-stopping', ({ agent, turn } = {}) => {
592
+ disposers.push(on('agent/turn-stopping', (payload = {}, _next, invocation) => {
593
+ const { agent, turn } = payload
532
594
  try {
595
+ if (!ruleMatches(config.rule, invocation ?? ruleFrame('agent/turn-stopping', [payload], warnOnce))) return
533
596
  const session = agent?.session
534
597
  if (session?.id === undefined || typeof agent.steer !== 'function') return
535
598
  if (!matchesAgentScope(config, agent)) return
@@ -567,33 +630,39 @@ function mainSessionForChild(ctx, id) {
567
630
  }
568
631
 
569
632
  /** 子代理生命周期:启动注入子代理;结束默认观察,可显式给所属主会话投递上下文。 */
570
- function wireSubagentEvents(ctx, configs, warnOnce) {
633
+ function wireSubagentEvents(ctx, configs, warnOnce, on) {
571
634
  const disposers = []
572
635
  const startConfigs = configs.filter((config) => config.layer === 'subagent-start')
573
636
  const endConfigs = configs.filter((config) => config.layer === 'subagent-end')
574
- const injectMain = endConfigs.some((config) => config.params?.action === 'inject-main')
575
637
  // B4 保留原状(不迁 `sessionState`):键是 `runId`(运行标识)而不是 session,不属于
576
638
  // 「按会话索引」的会话态,套统一接口会把语义挪到错误的键上;它的 `delete(最旧)` 也是与
577
639
  // `clear()` 不同的另一档淘汰策略,无等价证明前保留。
578
640
  const runs = new Map()
579
- if (injectMain) {
641
+ if (endConfigs.length > 0) {
580
642
  disposers.push(ctx.on('subagent/start', (info) => {
581
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
582
647
  const mainId = mainSessionForChild(ctx, info.id)
583
- if (mainId === undefined) return
584
648
  // ponytail: 有界运行记录;极端并发超过上限时,结束事件仍可通过存活会话血缘定位。
585
649
  if (runs.size >= MAX_TRACKED_SESSIONS) runs.delete(runs.keys().next().value)
586
- 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 })
587
651
  }))
588
- ctx.effect?.(() => () => runs.clear())
652
+ const clearRuns = () => runs.clear()
653
+ disposers.push(clearRuns)
654
+ ctx.effect?.(() => clearRuns)
589
655
  }
590
656
  if (startConfigs.length > 0) {
591
- disposers.push(ctx.on('subagent/start', (info) => {
657
+ disposers.push(on('subagent/start', (info, _next, invocation) => {
592
658
  try {
593
659
  const child = getService(ctx, 'agents')?.get?.(info?.id)
594
660
  if (child === undefined) return
661
+ const frame = invocation ?? ruleFrame('subagent/start', [info], warnOnce)
662
+ frame.subject.agent = child
595
663
  const subagentText = subagentTextOf(info)
596
664
  for (const config of startConfigs) {
665
+ if (!ruleMatches(config.rule, frame)) continue
597
666
  if (!matchesAgentScope(config, child)) continue
598
667
  if (!conditionHit(config, { subagentText })) continue
599
668
  if (typeof child.inject !== 'function') continue
@@ -607,14 +676,19 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
607
676
  }))
608
677
  }
609
678
  if (endConfigs.length > 0) {
610
- disposers.push(ctx.on('subagent/end', (info) => {
679
+ disposers.push(on('subagent/end', (info, _next, invocation) => {
611
680
  try {
612
681
  const child = getService(ctx, 'agents')?.get?.(info?.id)
613
682
  const recorded = runs.get(info?.runId)
614
683
  runs.delete(info?.runId)
615
684
  if (recorded !== undefined && recorded.id !== info?.id) return
616
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
617
690
  for (const config of endConfigs) {
691
+ if (!ruleMatches(config.rule, frame)) continue
618
692
  if (!matchesModel(config.modelScope, child?.options?.model ?? recorded?.model)) continue
619
693
  if (!conditionHit(config, { subagentText })) continue
620
694
  if (config.params?.action !== 'inject-main') {
@@ -652,18 +726,30 @@ function wireSubagentEvents(ctx, configs, warnOnce) {
652
726
  * @returns 聚合 disposer:回收本次接线显式创建的 waterfall 监听器;段/上下文/
653
727
  * 变量注册走 keepDisposer(随 ctx fiber 释放),不在本函数的回收面内。
654
728
  */
655
- export function wireLayers(ctx, configs, warnOnce) {
729
+ export function wireLayers(ctx, configs, warnOnce, options = {}) {
656
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))
657
739
  // 官方插值两层共享一份变量注册:运行时事实按 assembly 求值,非法名走别名改写。
658
- 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))
659
743
  const registered = [
660
- wireSystemSections(ctx, configs.filter((config) => config.layer === 'system-section'), registry, warnOnce),
661
- wireRuntimeContexts(ctx, configs.filter((config) => config.layer === 'runtime-context'), registry, warnOnce),
662
- wireAgentRequests(ctx, configs.filter((config) => config.layer === 'agent-request'), warnOnce),
663
- wireLlmStreams(ctx, configs.filter((config) => config.layer === 'llm-stream'), warnOnce),
664
- wireToolPipelines(ctx, configs.filter((config) => config.layer === 'tool-pipeline'), warnOnce),
665
- wireTurnStops(ctx, configs.filter((config) => config.layer === 'turn-stop'), warnOnce),
666
- 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,
667
753
  ].flat().filter((disposer) => typeof disposer === 'function')
668
754
  return () => {
669
755
  for (const dispose of registered.splice(0)) {
package/engine/order.mjs CHANGED
@@ -8,7 +8,10 @@ export function compareConfigSequence(a, b) {
8
8
  const difference = (a.sequence ?? a.order ?? 0) - (b.sequence ?? b.order ?? 0)
9
9
  if (difference !== 0) return difference
10
10
  if (a.sourceModuleId === undefined && b.sourceModuleId === undefined) return 0
11
- return compareText(a.sourceModuleId ?? '', b.sourceModuleId ?? '') || compareText(a.id, b.id)
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)
12
15
  }
13
16
 
14
17
  /** 官方文本段的定位仍由 order 决定;序号只提供同位置的稳定顺序。 */