dsh-plugin-prompt-tool 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/README.md +100 -233
  2. package/engine/actions/append-context.mjs +3 -24
  3. package/engine/actions/assembly.mjs +70 -94
  4. package/engine/actions/catalog.mjs +65 -28
  5. package/engine/actions/content.mjs +8 -152
  6. package/engine/actions/examples.mjs +8 -4
  7. package/engine/actions/inbox-prepend.mjs +4 -5
  8. package/engine/actions/index.mjs +17 -9
  9. package/engine/actions/inject-text.mjs +24 -4
  10. package/engine/actions/pre-step-filter.mjs +3 -4
  11. package/engine/actions/registration.mjs +20 -28
  12. package/engine/actions/request-params.mjs +21 -1
  13. package/engine/actions/shared.mjs +22 -13
  14. package/engine/actions/stream-output.mjs +26 -0
  15. package/engine/actions/tool-gate.mjs +31 -0
  16. package/engine/actions/tool-result.mjs +36 -0
  17. package/engine/actions/tool-visibility.mjs +248 -0
  18. package/engine/agent-capability.mjs +67 -0
  19. package/engine/anchor-match.mjs +14 -21
  20. package/engine/audience.d.mts +2 -0
  21. package/engine/audience.mjs +102 -0
  22. package/engine/branch.d.mts +4 -0
  23. package/engine/branch.mjs +61 -18
  24. package/engine/compaction-epoch.d.mts +25 -0
  25. package/engine/compaction-epoch.mjs +27 -4
  26. package/engine/compositions/source/local/filesystem-editor.yml +3 -3
  27. package/engine/compositions/source/local/instruction-hint.yml +2 -4
  28. package/engine/compositions/source/local/module-file-tools.yml +2 -0
  29. package/engine/compositions/source/local/module-import-tools.yml +4 -0
  30. package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
  31. package/engine/compositions/source/local/module-rule-tools.yml +5 -0
  32. package/engine/compositions/source/local/module-variable-tools.yml +5 -0
  33. package/engine/compositions/source/local/present-as.yml +26 -0
  34. package/engine/compositions/source/local/tool-config-engine.yml +2 -3
  35. package/engine/compositions/source/local/tool-search.yml +11 -0
  36. package/engine/condition-facts.mjs +84 -0
  37. package/engine/conditions/anchor.mjs +63 -23
  38. package/engine/conditions/availability.mjs +1 -1
  39. package/engine/conditions/composite.mjs +3 -3
  40. package/engine/conditions/count.mjs +42 -11
  41. package/engine/conditions/evaluation.mjs +74 -19
  42. package/engine/conditions/examples.mjs +2 -1
  43. package/engine/conditions/index.d.mts +15 -0
  44. package/engine/conditions/index.mjs +23 -9
  45. package/engine/conditions/operators.mjs +4 -0
  46. package/engine/conditions/phase.mjs +9 -3
  47. package/engine/conditions/preset.mjs +1 -1
  48. package/engine/conditions/session.mjs +8 -2
  49. package/engine/conditions/subject.mjs +144 -65
  50. package/engine/conditions/text.mjs +26 -8
  51. package/engine/conditions/variable.mjs +83 -0
  52. package/engine/context-activation.d.mts +11 -0
  53. package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
  54. package/engine/delivery.d.mts +2 -0
  55. package/engine/delivery.mjs +93 -0
  56. package/engine/discovery.d.mts +38 -0
  57. package/engine/discovery.mjs +146 -0
  58. package/engine/env-safety.mjs +14 -0
  59. package/engine/executor.d.mts +28 -0
  60. package/engine/executor.mjs +290 -205
  61. package/engine/fillers.mjs +39 -8
  62. package/engine/fixed-registration.mjs +12 -0
  63. package/engine/history.mjs +71 -0
  64. package/engine/instruction-blocks.d.mts +13 -0
  65. package/engine/instruction-blocks.mjs +21 -0
  66. package/engine/instruction-files.d.mts +5 -0
  67. package/engine/instruction-files.mjs +97 -0
  68. package/engine/instruction-hint.mjs +69 -103
  69. package/engine/interpolate.d.mts +7 -0
  70. package/engine/interpolate.mjs +44 -13
  71. package/engine/layers.mjs +163 -227
  72. package/engine/machine-paths.mjs +40 -0
  73. package/engine/module-file-tools.mjs +8 -0
  74. package/engine/module-import-tools.mjs +10 -0
  75. package/engine/module-manifest-tools.mjs +8 -0
  76. package/engine/module-rule-tools.mjs +10 -0
  77. package/engine/module-variable-tools.mjs +10 -0
  78. package/engine/order.d.mts +14 -0
  79. package/engine/order.mjs +0 -3
  80. package/engine/path-safety.d.mts +2 -0
  81. package/engine/path-safety.mjs +58 -0
  82. package/engine/pre-step-source.d.mts +69 -0
  83. package/engine/pre-step-source.mjs +103 -0
  84. package/engine/present-as.mjs +77 -0
  85. package/engine/promotion-pool.d.mts +9 -0
  86. package/engine/promotion-pool.mjs +69 -0
  87. package/engine/record.mjs +2 -0
  88. package/engine/rule-engine.mjs +3 -2
  89. package/engine/rule-facts.d.mts +11 -0
  90. package/engine/rule-facts.mjs +60 -0
  91. package/engine/rule-runtime.d.mts +46 -0
  92. package/engine/rule-runtime.mjs +98 -13
  93. package/engine/rule-spec.d.mts +106 -0
  94. package/engine/rule-spec.mjs +301 -27
  95. package/engine/run-code-env.mjs +73 -19
  96. package/engine/schema.mjs +165 -243
  97. package/engine/sdk-strip.mjs +2 -2
  98. package/engine/service-names.d.mts +10 -0
  99. package/engine/service-names.mjs +10 -0
  100. package/engine/shared.d.mts +21 -0
  101. package/engine/shared.mjs +46 -13
  102. package/engine/skill-search.mjs +125 -53
  103. package/engine/source-kind.d.mts +9 -0
  104. package/engine/source-kind.mjs +16 -0
  105. package/engine/strategy-ids.mjs +5 -0
  106. package/engine/template-evaluation.d.mts +16 -0
  107. package/engine/template-evaluation.mjs +178 -0
  108. package/engine/template-frame.d.mts +16 -0
  109. package/engine/template-frame.mjs +69 -0
  110. package/engine/tool-config-engine.mjs +38 -34
  111. package/engine/tool-definition.d.mts +4 -0
  112. package/engine/tool-definition.mjs +5 -1
  113. package/engine/tool-git-bash.mjs +5 -1
  114. package/engine/tool-scope.mjs +22 -0
  115. package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
  116. package/engine/trigger.mjs +15 -270
  117. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  118. package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
  119. package/engine/vendor/yaml/package.json +1 -1
  120. package/lib/client.js +8312 -6724
  121. package/lib/client.js.map +1 -1
  122. package/lib/index.d.mts +16 -2120
  123. package/lib/index.mjs +26914 -5816
  124. package/modules/ponytail/module.yml +83 -203
  125. package/modules/ptc-mode/module.yml +45 -0
  126. package/modules/soul/assets/core-hard.md +6 -0
  127. package/modules/soul/assets/criteria.md +12 -0
  128. package/modules/soul/assets/echo.md +12 -0
  129. package/modules/soul/assets/layers.md +11 -0
  130. package/modules/soul/assets/ledger.md +9 -0
  131. package/modules/soul/assets/manifest.yml +20 -0
  132. package/modules/soul/assets/reference/00-layers.md +27 -0
  133. package/modules/soul/assets/reference/01-criteria.md +32 -0
  134. package/modules/soul/assets/reference/02-echo.md +158 -0
  135. package/modules/soul/assets/reference/03-vital.md +183 -0
  136. package/modules/soul/assets/reference/04-manifest.json +106 -0
  137. package/modules/soul/assets/reference/05-doctor.md +67 -0
  138. package/modules/soul/assets/reference/06-reference.md +189 -0
  139. package/modules/soul/assets/reference/07-bundle.json +431 -0
  140. package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
  141. package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
  142. package/modules/soul/assets/reference/10-echo-anatomy.md +100 -0
  143. package/modules/soul/assets/reference/11-self.md +80 -0
  144. package/modules/soul/assets/reference/12-ledger.md +159 -0
  145. package/modules/soul/assets/reference/README.md +10 -0
  146. package/modules/soul/assets/templates/agents-template.md +6 -0
  147. package/modules/soul/assets/vital.md +9 -0
  148. package/modules/soul/module.yml +311 -0
  149. package/modules/superpowers/module.yml +122 -0
  150. package/modules/tool-surface/module.yml +51 -47
  151. package/package.json +37 -40
  152. package/skills/dsh-module/SKILL.md +67 -147
  153. package/skills/dsh-module/reference.md +113 -86
  154. package/templates/10-pre-step.yml +2 -3
  155. package/templates/14-first-turn-anchor.yml +10 -14
  156. package/templates/15-guide-auto.yml +32 -28
  157. package/templates/16-custom-fallback.yml +33 -21
  158. package/templates/20-system-section.yml +1 -4
  159. package/templates/50-llm-stream.yml +4 -10
  160. package/templates/60-tool-pipeline.yml +12 -10
  161. package/templates/63-append-context.yml +4 -6
  162. package/templates/65-turn-stop.yml +2 -2
  163. package/templates/67-inbox-prepend.yml +2 -2
  164. package/templates/70-subagent-maintenance.yml +9 -10
  165. package/templates/75-subagent-end.yml +2 -2
  166. package/templates/80-tool-surface.yml +41 -57
  167. package/templates/tools/40-delegate-module-import.yml +22 -0
  168. package/templates/tools/50-delegate-module-rules.yml +14 -0
  169. package/templates/tools/60-delegate-module-variables.yml +17 -0
  170. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  171. package/engine/actions/decision.mjs +0 -54
  172. package/engine/actions/guard.mjs +0 -80
  173. package/engine/actions/sdk-strip.mjs +0 -30
  174. package/engine/character-tools.mjs +0 -8
  175. package/engine/classify-task.mjs +0 -53
  176. package/engine/compositions/source/local/character-tools.yml +0 -5
  177. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  178. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  179. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  180. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  181. package/engine/condition.mjs +0 -96
  182. package/engine/declared-triggers.mjs +0 -93
  183. package/engine/session-var-tools.mjs +0 -8
  184. package/engine/session-vars.mjs +0 -54
  185. package/engine/st-macros.d.mts +0 -22
  186. package/engine/st-macros.mjs +0 -208
  187. package/engine/st-render.mjs +0 -109
  188. package/engine/st-world-book.d.mts +0 -10
  189. package/engine/subagent-tool-policy-core.mjs +0 -400
  190. package/engine/subagent-tool-policy.mjs +0 -352
  191. package/engine/trigger-spec.mjs +0 -173
  192. package/engine/world-book-tools.mjs +0 -8
  193. package/lib/migrate-rules.d.mts +0 -1
  194. package/lib/migrate-rules.mjs +0 -610
  195. package/lib/module-package-DqJZaE71.mjs +0 -2
  196. package/lib/module-package-q2gDRu9H.mjs +0 -2869
  197. package/lib/rule-builder-DMrJten6.mjs +0 -14767
  198. package/templates/61-deny-tools-presentation.yml +0 -27
  199. package/templates/62-guard-deny-tools.yml +0 -20
  200. package/templates/policies/legacy-defaults.yml +0 -34
  201. package/templates/tools/40-delegate-character.yml +0 -15
  202. package/templates/tools/50-delegate-worldbook.yml +0 -15
  203. package/templates/tools/60-delegate-session-var.yml +0 -15
  204. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -7,8 +7,10 @@ import { createSessionStatePredicate } from './session.mjs'
7
7
  import { createPresetPredicate } from './preset.mjs'
8
8
  import { createScopePredicate } from './scope.mjs'
9
9
  import { createAnchorPredicate } from './anchor.mjs'
10
+ import { createVariablePredicate } from './variable.mjs'
10
11
  import { withAvailableInput } from './availability.mjs'
11
12
  import { composite } from './composite.mjs'
13
+ import { COMPOSITE_OPERATORS } from './operators.mjs'
12
14
  export * from './text.mjs'
13
15
  export * from './phase.mjs'
14
16
  export * from './source.mjs'
@@ -18,7 +20,8 @@ export * from './session.mjs'
18
20
  export * from './preset.mjs'
19
21
  export * from './scope.mjs'
20
22
  export * from './anchor.mjs'
21
- export { subjectOf } from './subject.mjs'
23
+ export * from './variable.mjs'
24
+ export { subjectOf, channelTextSubjects, channelFactSubjects, FACT_PREDICATE_SUBJECTS } from './subject.mjs'
22
25
  export { composite } from './composite.mjs'
23
26
  export const PREDICATE_FACTORIES = Object.freeze({
24
27
  text: createTextPredicate,
@@ -30,13 +33,14 @@ export const PREDICATE_FACTORIES = Object.freeze({
30
33
  preset: createPresetPredicate,
31
34
  scope: createScopePredicate,
32
35
  anchor: createAnchorPredicate,
36
+ variable: createVariablePredicate,
33
37
  })
34
- export const COMPOSITE_OPERATORS = Object.freeze(['any', 'all', 'not', 'notAny'])
38
+ export { COMPOSITE_OPERATORS }
35
39
 
36
40
  const objectOrUndefined = (value, label) => {
37
41
  if (value === undefined || value === null) return undefined
38
42
  if (typeof value !== 'object' || Array.isArray(value)) {
39
- throw new TypeError(`trigger-spec: ${label} must be an object`)
43
+ throw new TypeError(`rule.if: ${label} must be an object`)
40
44
  }
41
45
  return value
42
46
  }
@@ -44,15 +48,15 @@ const objectOrUndefined = (value, label) => {
44
48
  export function compileWhen(node, context = {}) {
45
49
  if (node === undefined || node === null) return undefined
46
50
  if (typeof node !== 'object' || Array.isArray(node)) {
47
- throw new TypeError('trigger-spec: when must be an object (a composite node or a predicate node)')
51
+ throw new TypeError('rule.if: when must be an object (a composite node or a predicate node)')
48
52
  }
49
53
  const keys = Object.keys(node)
50
54
  if (keys.length === 0) {
51
- throw new TypeError('trigger-spec: when must not be an empty object — 空节点是「恒真」还是「写漏了」无法区分')
55
+ throw new TypeError('rule.if: when must not be an empty object — 空节点是「恒真」还是「写漏了」无法区分')
52
56
  }
53
57
  if (keys.length > 1) {
54
58
  throw new TypeError(
55
- `trigger-spec: when must declare exactly one key — got ${keys.sort().join(', ')}`
59
+ `rule.if: when must declare exactly one key — got ${keys.sort().join(', ')}`
56
60
  + `(allowed: ${[...COMPOSITE_OPERATORS, ...Object.keys(PREDICATE_FACTORIES)].sort().join(', ')})`,
57
61
  )
58
62
  }
@@ -64,7 +68,7 @@ export function compileWhen(node, context = {}) {
64
68
  return composite({ not: compileWhen(value, context) })
65
69
  }
66
70
  if (!Array.isArray(value) || value.length === 0) {
67
- throw new TypeError(`trigger-spec: when.${key} must be a non-empty array of nodes`)
71
+ throw new TypeError(`rule.if: when.${key} must be a non-empty array of nodes`)
68
72
  }
69
73
  return composite({ [key]: value.map((child) => compileWhen(child, context)) })
70
74
  }
@@ -72,14 +76,24 @@ export function compileWhen(node, context = {}) {
72
76
  const factory = PREDICATE_FACTORIES[key]
73
77
  if (factory === undefined) {
74
78
  throw new TypeError(
75
- `trigger-spec: unknown predicate ${JSON.stringify(key)}`
79
+ `rule.if: unknown predicate ${JSON.stringify(key)}`
76
80
  + ` — known predicates: ${Object.keys(PREDICATE_FACTORIES).join(', ')}; composite: ${COMPOSITE_OPERATORS.join(', ')}`,
77
81
  )
78
82
  }
79
83
  const options = objectOrUndefined(value, `when.${key}`) ?? {}
80
- // `preset` 原语的取法需要挂载期注入(见 predicates.mjs 的三条硬约束),其余原语只用选项。
84
+ // 条件上下文只补宿主查询、共享晋升池和同规则动作引用,不建立第二套运行状态。
85
+ if (key === 'anchor') {
86
+ return withAvailableInput(key, options, createAnchorPredicate(options, context))
87
+ }
81
88
  if (key === 'preset') {
82
89
  return withAvailableInput(key, options, createPresetPredicate({ ...options, ctx: context.ctx, standingMountFor: context.standingMountFor }))
83
90
  }
91
+ if (key === 'phase') {
92
+ return withAvailableInput(key, options, createPhasePredicate({ ...options, promotionPool: context.promotionPool }))
93
+ }
94
+ // 变量谓词自带三值处理:适配器缺失返回 UNAVAILABLE,不走通道事实的 availableInput。
95
+ if (key === 'variable') {
96
+ return createVariablePredicate(options, { store: context.variableStore, moduleId: context.moduleId })
97
+ }
84
98
  return withAvailableInput(key, options, factory(options))
85
99
  }
@@ -0,0 +1,4 @@
1
+ /** 条件树组合算子的唯一清单:纯 ESM,无宿主 import,client 可打包。 */
2
+ import { MATCH_LOGIC } from '../anchor-match.mjs'
3
+
4
+ export const COMPOSITE_OPERATORS = Object.freeze(Object.values(MATCH_LOGIC))
@@ -18,7 +18,13 @@ export function createPhasePredicate(options = {}) {
18
18
  reasoningNegativePattern: options.reasoningNegativePattern,
19
19
  reasoningFlags: options.reasoningFlags,
20
20
  }
21
- const tracker = createEpochPromotion(promoteEvents, trackerOptions)
21
+ // 订阅模式:追踪器从挂载缝的池里取(同配置复用、事件由池统一喂),本谓词**不再提供 observe**
22
+ // ——共用实例后重复喂会让门控步数翻倍(`applyEvent` 的 `steps += 1` 不幂等)。
23
+ // 池缺席(不经挂载缝直接调 `compileWhen` 的调用方,例如谓词单测):退回自建并自带 observe,
24
+ // 行为与从前一致。不订阅模式(作者显式要求):保持每次判定冷扫、且不给 observe——那正是
25
+ // 「不订阅」的语义,多一个入口只会诱使调用方以为状态被记住了。
26
+ const pooled = subscribe ? options.promotionPool : undefined
27
+ const tracker = pooled === undefined ? createEpochPromotion(promoteEvents, trackerOptions) : pooled.tracker(promoteEvents, trackerOptions)
22
28
  // 载荷归一后 agent 在 `payload.agent`;旧的手工形态(载荷本身就是 agent)由 agentOf 兼容。
23
29
  //
24
30
  // `compacted`(2026-09-22 用户拍板「扩原语」):`status()` 本来就返回 `boundary`
@@ -47,7 +53,7 @@ export function createPhasePredicate(options = {}) {
47
53
  return status.promoted === (promotedOption ?? true)
48
54
  }
49
55
  predicate.kind = 'phase'
50
- // 只有订阅模式才有增量入口:不订阅时多一个入口只会诱使调用方以为状态被记住了。
51
- if (subscribe) predicate.observe = (session, event) => tracker.observe(session, event)
56
+ // 只有「自建 + 订阅」才提供增量入口:池已经统一喂过,再给入口就是重复推进相位。
57
+ if (subscribe && pooled === undefined) predicate.observe = (session, event) => tracker.observe(session, event)
52
58
  return predicate
53
59
  }
@@ -46,7 +46,7 @@ export function agentPresetId(ctx, agent, standingMountFor) {
46
46
  const mount = typeof standingMountFor === 'function' ? standingMountFor(agent?.ctx) : undefined
47
47
  return mount?.presetId
48
48
  } catch {
49
- // 判定不抛出:拿不到就是「没有预设」(不命中),与 condition.mjs 的降级纪律一致。
49
+ // 判定不抛出:拿不到就是「没有预设」(不命中),与 condition-facts.mjs 的降级纪律一致。
50
50
  return undefined
51
51
  }
52
52
  }
@@ -1,4 +1,5 @@
1
- import { isDelegated, sessionEvents } from '../shared.mjs'
1
+ import { SURFACE_MESSAGE_TYPES, currentEvents, historyEvents } from '../history.mjs'
2
+ import { isDelegated } from '../shared.mjs'
2
3
  import { sessionOf } from './subject.mjs'
3
4
  import { optionalBoolean } from './values.mjs'
4
5
 
@@ -12,9 +13,14 @@ export function createSessionStatePredicate(options = {}) {
12
13
  // `true` = 只在子代理命中,未声明 = 不判受众。子代理判定复用 `shared.isDelegated`,
13
14
  // 不写字面量比较(trigger.mjs 的会话态契约第 (2) 条纪律)。
14
15
  const delegated = optionalBoolean(options.delegated, 'delegated', undefined)
16
+ // `present` 对消息类镜像的是**当前上下文**,不是「本会话发生过」:被压缩遮蔽的事件模型已经看不到。
17
+ // 非 surface 承载的类型(`tool/call`、`turn/start`、`compaction/end`)永远不是 surface 节点,
18
+ // 按当前上下文判只会恒 false(`present: true`)/ 恒 true(`present: false`)——配置能编译能挂载,
19
+ // 却永不按意图命中,所以这类类型按完整历史判(类型表见 `history.mjs#SURFACE_MESSAGE_TYPES`)。
20
+ const view = SURFACE_MESSAGE_TYPES.has(type) ? currentEvents : historyEvents
15
21
  const predicate = (payload) => {
16
22
  if (delegated !== undefined && isDelegated(sessionOf(payload)) !== delegated) return false
17
- return sessionEvents(sessionOf(payload))
23
+ return view(sessionOf(payload))
18
24
  .some((event) => event?.type === type) === present
19
25
  }
20
26
  predicate.kind = 'session'
@@ -1,30 +1,6 @@
1
- import { lastAssistantText, subagentTextOf, toolArgsText, userMessagesText } from '../condition.mjs'
1
+ import { SUBJECT_FIELDS, lastAssistantText, subagentTextOf, toolArgsText, userMessagesText } from '../condition-facts.mjs'
2
2
  import { extractText } from '../shared.mjs'
3
3
 
4
- const CHANNEL_SUBJECTS = {
5
- 'system-prompt/assemble': (args) => ({ agent: args[1]?.agent }),
6
- 'agent/request': (args) => ({ agent: args[0]?.agent }),
7
- 'agent/pre-step': (args) => ({ agent: args[0]?.agent }),
8
- 'agent/inbox/inserted': (args) => ({ agent: args[0]?.agent, source: args[0]?.message?.source }),
9
- 'agent/turn-stopping': (args) => ({ agent: args[0]?.agent }),
10
- 'tools/pre-execute': (args) => ({ agent: args[0]?.agent, name: args[0]?.name }),
11
- 'tools/post-execute': (args) => ({ agent: args[0]?.agent, name: args[0]?.name }),
12
- 'session/event': (args) => ({ session: args[0] }),
13
- 'llm/stream': (args) => ({ model: args[0]?.model }),
14
- 'subagent/start': () => ({}),
15
- 'subagent/end': () => ({}),
16
- }
17
-
18
- /** 未知通道的告警去重(每个通道一次;判定期不刷屏)。 */
19
- const warnedChannels = new Set()
20
-
21
- /**
22
- * 归一化载荷的标记(Symbol,非字符串键):`subjectOf` 产出它,`agentOf`/`sessionOf` 据此
23
- * 区分「归一化载荷」与「旧形态的直接 payload」——**不用启发式猜**(曾按「有没有 `.session`」
24
- * 判断,结果把测试里 `{ id, ctx }` 形状的假 agent 误判成归一化载荷)。
25
- */
26
- const SUBJECT_MARK = Symbol('predicate-subject')
27
-
28
4
  /**
29
5
  * 子代理事件判定期可见的 agent 事实。
30
6
  *
@@ -34,22 +10,139 @@ const SUBJECT_MARK = Symbol('predicate-subject')
34
10
  * 字符串)与 `scope.audience`(要求 session 可达)在子代理两层恒判 UNAVAILABLE——UI 上
35
11
  * 配了却永远不命中,正是 schema 注释要避免的「配了没效果也不报错」。
36
12
  *
37
- * 这里在**判定前**用事件里的 `id` 反查 agent 补齐同样的事实。取不到时不写键:判定
38
- * 退回 UNAVAILABLE(fail-closed),不得乐观放行。
13
+ * 这里在**判定前**用事件里的 `id` 反查 agent 补齐同样的事实。取不到时返回 `undefined`
14
+ * (取值器不写键):判定退回 UNAVAILABLE(fail-closed),不得乐观放行。
15
+ */
16
+ const subagentAgent = (args, ctx) => {
17
+ const id = args[0]?.id
18
+ if (typeof id !== 'string' || id.length === 0) return undefined
19
+ return ctx?.agents?.get?.(id) ?? undefined
20
+ }
21
+
22
+ const streamAgent = (args, ctx) => {
23
+ const call = args[0]
24
+ if (call?.agent !== undefined) return call.agent
25
+ if (typeof call?.sessionId !== 'string') return undefined
26
+ return (ctx?.get?.('agents') ?? ctx?.agents)?.get?.(call.sessionId)
27
+ }
28
+
29
+ /**
30
+ * 子代理两层的事实。`name` = provider(`SubagentRunInfo` 里唯一稳定的分类事实:
31
+ * spawn / fork / acp / codex / claude-code / dsh-sdk),`names` 谓词读的就是它,
32
+ * 因此 `names` 在这两层合法(编译期据此放行)。provider 可能缺席(官方注释:已接受的
33
+ * one-shot 变 ready 或持久 Activation 冷恢复时提供方未必仍注册)——那时返回 `undefined`
34
+ * 让取值器不写键、`names` 走 **运行期** UNAVAILABLE,而不是塞空串当「已知为空」,
35
+ * 也不是编译期拒绝。
36
+ */
37
+ const SUBAGENT_FACTS = {
38
+ name: (args) => {
39
+ const provider = args[0]?.provider
40
+ return typeof provider === 'string' && provider.length > 0 ? provider : undefined
41
+ },
42
+ agent: subagentAgent,
43
+ session: (args, ctx) => subagentAgent(args, ctx)?.session,
44
+ model: (args, ctx) => {
45
+ const model = subagentAgent(args, ctx)?.options?.model
46
+ return typeof model === 'string' && model.length > 0 ? model : undefined
47
+ },
48
+ }
49
+
50
+ /**
51
+ * 通道 → 归一化载荷的字段与取值器:**唯一**的「该通道提供什么事实」真相。
52
+ * `channelTextSubjects` 与 `channelFactSubjects` 都从这张表派生,不另立第二张手抄表。
53
+ *
54
+ * - `text[字段名] = args => value`:字段名取自 `condition-facts.mjs#SUBJECT_FIELDS`,
55
+ * 经 `SUBJECT_OF_FIELD` 反查成文本 subject。
56
+ * - `facts[字段名] = (args, ctx) => value`:字段名本身即 subject(`name` / `source` /
57
+ * `agent` / `session` / `model`);返回 `undefined` 即不写键(判定退回 UNAVAILABLE)。
39
58
  */
40
- function subagentFacts(info, ctx) {
41
- const facts = {}
42
- const id = info?.id
43
- if (typeof id !== 'string' || id.length === 0) return facts
44
- const agent = ctx?.agents?.get?.(id)
45
- if (agent === undefined || agent === null) return facts
46
- facts.agent = agent
47
- if (agent.session !== undefined) facts.session = agent.session
48
- const model = agent.options?.model
49
- if (typeof model === 'string' && model.length > 0) facts.model = model
50
- return facts
59
+ const CHANNELS = {
60
+ 'system-prompt/assemble': { facts: { agent: args => args[1]?.agent } },
61
+ 'agent/request': { facts: { agent: args => args[0]?.agent } },
62
+ 'agent/pre-step': {
63
+ facts: { agent: args => args[0]?.agent },
64
+ text: { userText: args => userMessagesText(args[0]?.messages) },
65
+ },
66
+ 'agent/inbox/inserted': {
67
+ facts: { agent: args => args[0]?.agent, source: args => args[0]?.message?.source },
68
+ text: { userText: args => userMessagesText([args[0]?.message]) },
69
+ },
70
+ 'agent/turn-stopping': {
71
+ facts: { agent: args => args[0]?.agent },
72
+ text: { assistantText: args => lastAssistantText(args[0]?.agent?.session) },
73
+ },
74
+ 'tools/pre-execute': {
75
+ facts: { agent: args => args[0]?.agent, name: args => args[0]?.name },
76
+ text: { argsText: args => toolArgsText(args[0]?.arguments) },
77
+ },
78
+ 'tools/post-execute': {
79
+ facts: { agent: args => args[0]?.agent, name: args => args[0]?.name },
80
+ text: { argsText: args => toolArgsText(args[0]?.arguments), resultText: args => extractText(args[1]) },
81
+ },
82
+ 'session/event': { facts: { session: args => args[0] } },
83
+ 'llm/stream': { facts: { model: args => args[0]?.model, agent: streamAgent } },
84
+ 'subagent/start': { facts: SUBAGENT_FACTS, text: { subagentText: args => subagentTextOf(args[0]) } },
85
+ 'subagent/end': { facts: SUBAGENT_FACTS, text: { subagentText: args => subagentTextOf(args[0]) } },
51
86
  }
52
87
 
88
+ /** 字段名 → subject 反查;键名只在 SUBJECT_FIELDS 声明一次,表与名单交叉而不重复。 */
89
+ const SUBJECT_OF_FIELD = Object.fromEntries(Object.entries(SUBJECT_FIELDS).map(([subject, field]) => [field, subject]))
90
+
91
+ /**
92
+ * 事实谓词 → 它读取的事实 subject:校验面与运行面的**唯一**登记处——新谓词还要在
93
+ * `PREDICATE_FACTORIES` 与通道可用性表登记,但「谓词 → subject」只此一处,
94
+ * `rule-spec.mjs#conditionSubjects` 直接消费本对象、`FACT_SUBJECTS` 也由它派生。
95
+ * shortcut: 漂移断言靠运行时改写本对象;将来若 `Object.freeze`,需把判据换成「同一引用」。
96
+ */
97
+ export const FACT_PREDICATE_SUBJECTS = { names: 'name', source: 'source' }
98
+
99
+ /** 谓词能消费的事实 subject:`names` 读 `name`、`source` 读 `source`;agent/session/model 不是谓词 subject。 */
100
+ const FACT_SUBJECTS = Object.values(FACT_PREDICATE_SUBJECTS)
101
+
102
+ /**
103
+ * 本通道可用的文本 subject。未列出的通道(`system-prompt/assemble`、`agent/request`、
104
+ * `llm/stream`、`session/event`)不提供任何文本,其上声明的 `text` 条件永不命中。
105
+ * @param {string} channel 官方事件名
106
+ * @returns {string[]} 可用 subject
107
+ */
108
+ export function channelTextSubjects(channel) {
109
+ const fields = CHANNELS[channel]?.text
110
+ if (fields === undefined) return []
111
+ return Object.keys(fields).map(field => SUBJECT_OF_FIELD[field]).filter(subject => subject !== undefined)
112
+ }
113
+
114
+ /**
115
+ * 本通道可用的事实 subject(`name` / `source` 中它具备的那些)——编译期据此拒绝
116
+ * `names` / `source` 的死条件(见 `rule-spec.mjs`)。
117
+ * @param {string} channel 官方事件名
118
+ * @returns {string[]} 可用 subject
119
+ */
120
+ export function channelFactSubjects(channel) {
121
+ const facts = CHANNELS[channel]?.facts
122
+ if (facts === undefined) return []
123
+ return FACT_SUBJECTS.filter(subject => Object.hasOwn(facts, subject))
124
+ }
125
+
126
+ /** 按表取字段:`undefined` 不写键——判定据此退回 UNAVAILABLE,而不是把「取不到」当成「已知为空」。 */
127
+ function pickFields(readers, args, ctx) {
128
+ const out = {}
129
+ for (const [field, read] of Object.entries(readers ?? {})) {
130
+ const value = read(args, ctx)
131
+ if (value !== undefined) out[field] = value
132
+ }
133
+ return out
134
+ }
135
+
136
+ /** 未知通道的告警去重(每个通道一次;判定期不刷屏)。 */
137
+ const warnedChannels = new Set()
138
+
139
+ /**
140
+ * 归一化载荷的标记(Symbol,非字符串键):`subjectOf` 产出它,`agentOf`/`sessionOf` 据此
141
+ * 区分「归一化载荷」与「旧形态的直接 payload」——**不用启发式猜**(曾按「有没有 `.session`」
142
+ * 判断,结果把测试里 `{ id, ctx }` 形状的假 agent 误判成归一化载荷)。
143
+ */
144
+ const SUBJECT_MARK = Symbol('predicate-subject')
145
+
53
146
  /**
54
147
  * 把事件参数表归一为谓词载荷。
55
148
  *
@@ -68,36 +161,22 @@ export function subjectOf(channel, args, warn, ctx) {
68
161
  // `{ agent, turn }`)因此继续可见,既有断言不会因归一化而失效。
69
162
  const first = args[0]
70
163
  const base = first !== null && typeof first === 'object' && !Array.isArray(first) ? { ...first } : {}
71
- const pick = CHANNEL_SUBJECTS[channel]
72
- const text = (() => {
73
- switch (channel) {
74
- case 'tools/pre-execute': return { argsText: toolArgsText(args[0]?.arguments) }
75
- case 'tools/post-execute': return { argsText: toolArgsText(args[0]?.arguments), resultText: extractText(args[1]) }
76
- case 'agent/pre-step': return { userText: userMessagesText(args[0]?.messages) }
77
- case 'agent/inbox/inserted': return { userText: userMessagesText([args[0]?.message]) }
78
- case 'agent/turn-stopping': return { assistantText: lastAssistantText(args[0]?.agent?.session) }
79
- // 子代理事件的 `name` = provider(`SubagentRunInfo` 里唯一稳定的分类事实:
80
- // spawn / fork / acp / codex / claude-code / dsh-sdk)。`names` 谓词读的就是
81
- // `payload.name`,因此 `if: { names: { allow: ['fork'] } }` 在此可用。
82
- // provider 可能缺席(官方注释:已接受的 one-shot 变 ready 或持久 Activation 冷恢复时
83
- // 提供方未必仍注册)——那时不写键,让 `names` 走 UNAVAILABLE,而不是塞空串当「已知为空」。
84
- case 'subagent/start':
85
- case 'subagent/end': return {
86
- subagentText: subagentTextOf(args[0]),
87
- ...(typeof args[0]?.provider === 'string' && args[0].provider.length > 0 ? { name: args[0].provider } : {}),
88
- ...subagentFacts(args[0], ctx),
89
- }
90
- default: return {}
91
- }
92
- })()
93
- if (pick === undefined) {
164
+ const spec = CHANNELS[channel]
165
+ if (spec === undefined) {
94
166
  if (typeof warn === 'function' && !warnedChannels.has(channel)) {
95
167
  warnedChannels.add(channel)
96
168
  warn(`predicates: unknown channel ${JSON.stringify(channel)} — 无显式取法,退回「第一个实参即载荷」的旧语义`)
97
169
  }
98
- return { ...base, ...text, [SUBJECT_MARK]: true, channel, args }
170
+ return { ...base, [SUBJECT_MARK]: true, channel, args }
171
+ }
172
+ return {
173
+ ...base,
174
+ ...pickFields(spec.facts, args, ctx),
175
+ ...pickFields(spec.text, args, ctx),
176
+ [SUBJECT_MARK]: true,
177
+ channel,
178
+ args,
99
179
  }
100
- return { ...base, ...pick(args), ...text, [SUBJECT_MARK]: true, channel, args }
101
180
  }
102
181
 
103
182
  /**
@@ -111,8 +190,8 @@ export function agentOf(payload) {
111
190
 
112
191
  /**
113
192
  * 从载荷取 session:归一化载荷走 `session` → `agent.session`;旧形态走
114
- * `session` → 载荷自身。取不到时返回原载荷——`sessionEvents` 对无 `snapshotEvents`
115
- * 的对象返回空,语义是「无事件」,与归一化之前逐例一致。
193
+ * `session` → 载荷自身。取不到时返回原载荷——`history.mjs#historyEvents` / `#currentEvents`
194
+ * 对无 `snapshotEvents` 的对象返回空,语义是「无事件」,与归一化之前逐例一致。
116
195
  */
117
196
  export function sessionOf(payload) {
118
197
  if (payload === null || typeof payload !== 'object') return payload
@@ -1,15 +1,29 @@
1
1
  import { MATCH_LOGIC, createAnchorMatcher } from '../anchor-match.mjs'
2
- import { subjectTextOf } from '../condition.mjs'
2
+ import { subjectTextOf } from '../condition-facts.mjs'
3
3
  import { keyList } from './values.mjs'
4
4
  import { conditionInputAvailable, UNAVAILABLE } from './availability.mjs'
5
- const MATCH_FLAGS = ['caseSensitive', 'wholeWords', 'useRegex', 'stWords']
5
+ const MATCH_FLAGS = ['caseSensitive', 'wholeWords', 'useRegex']
6
6
  const MATCH_MODES = ['scan', 'prefix']
7
7
 
8
8
  export function createTextPredicate(options = {}) {
9
+ if (options.stWords !== undefined) throw new TypeError('stWords 已退役,请显式转换为 wordBoundary')
10
+ if (options.wordBoundary !== undefined && !['ascii-token', 'unicode'].includes(options.wordBoundary)) throw new TypeError('wordBoundary 必须是 ascii-token 或 unicode')
9
11
  const keys = keyList(options.keys, 'keys')
10
12
  const secondaryKeys = keyList(options.secondaryKeys, 'secondaryKeys')
11
- if (keys.length === 0 && secondaryKeys.length === 0) {
12
- throw new TypeError('predicates: a text predicate needs at least one non-empty key')
13
+ const bounds = ['minChars', 'maxChars'].map(field => {
14
+ const value = options[field]
15
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
16
+ throw new TypeError(`predicates: ${field} must be a non-negative safe integer`)
17
+ }
18
+ return value
19
+ })
20
+ const [minChars, maxChars] = bounds
21
+ if (minChars !== undefined && maxChars !== undefined && minChars > maxChars) {
22
+ throw new TypeError('predicates: minChars cannot exceed maxChars')
23
+ }
24
+ const hasKeys = keys.length > 0 || secondaryKeys.length > 0
25
+ if (!hasKeys && minChars === undefined && maxChars === undefined) {
26
+ throw new TypeError('predicates: a text predicate needs a non-empty key or minChars/maxChars')
13
27
  }
14
28
  const logic = options.logic ?? MATCH_LOGIC.ANY
15
29
  if (!Object.values(MATCH_LOGIC).includes(logic)) {
@@ -33,12 +47,16 @@ export function createTextPredicate(options = {}) {
33
47
  useRegex: options.useRegex,
34
48
  logic,
35
49
  mode,
36
- stWords: options.stWords === true,
50
+ wordBoundary: options.wordBoundary,
37
51
  })
38
52
  const subject = options.subject
39
- const predicate = (payload) => conditionInputAvailable('text', options, payload)
40
- ? matcher.scan(typeof payload === 'string' ? payload : subjectTextOf(subject, payload)).active === true
41
- : UNAVAILABLE
53
+ const predicate = (payload) => {
54
+ if (!conditionInputAvailable('text', options, payload)) return UNAVAILABLE
55
+ const text = typeof payload === 'string' ? payload : subjectTextOf(subject, payload)
56
+ return (minChars === undefined || text.length >= minChars)
57
+ && (maxChars === undefined || text.length <= maxChars)
58
+ && (!hasKeys || matcher.scan(text).active === true)
59
+ }
42
60
  predicate.kind = 'text'
43
61
  return predicate
44
62
  }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * variable 谓词:把模块级持久变量接进规则条件。
3
+ *
4
+ * 适配器由挂载期绑定(`variableStore.read(moduleId)` → { module, global, effective, enabled }),
5
+ * 与模板求值共用同一个 owner;编译期没有适配器,判定为 UNAVAILABLE(三值语义,不能经 not 反转)。
6
+ */
7
+ import { UNAVAILABLE } from './availability.mjs'
8
+
9
+ const OPS = new Set(['eq', 'ne', 'in', 'gt', 'gte', 'lt', 'lte', 'present', 'absent'])
10
+ const SCOPES = new Set(['effective', 'module', 'global'])
11
+
12
+ /** 严格数字:空串与空白不算数字(`Number('')` 是 0,是陷阱);非有限数不算数字。 */
13
+ function numeric(text) {
14
+ if (text.trim().length === 0) return undefined
15
+ const value = Number(text)
16
+ return Number.isFinite(value) ? value : undefined
17
+ }
18
+
19
+ /**
20
+ * @param {object} options
21
+ * @param {string} options.key 变量名(模块声明,引擎不认前缀)
22
+ * @param {'eq'|'ne'|'in'|'gt'|'gte'|'lt'|'lte'|'present'|'absent'} [options.op] 缺省 eq
23
+ * @param {string|string[]} [options.value] in 用数组;present/absent 不收
24
+ * @param {'effective'|'module'|'global'} [options.scope] 缺省 effective(模块覆盖全局)
25
+ * @param {{ store?: { read: (moduleId: string) => { module?: Record<string, string>, global?: Record<string, string>, effective?: Record<string, string>, enabled?: boolean } }, moduleId?: string }} [context]
26
+ */
27
+ export function createVariablePredicate(options = {}, context = {}) {
28
+ const key = options.key
29
+ if (typeof key !== 'string' || key.length === 0) {
30
+ throw new TypeError('predicates: variable key must be a non-empty string')
31
+ }
32
+ const op = options.op ?? 'eq'
33
+ if (!OPS.has(op)) {
34
+ throw new TypeError(`predicates: variable op must be one of ${[...OPS].join(', ')}`)
35
+ }
36
+ const scope = options.scope ?? 'effective'
37
+ if (!SCOPES.has(scope)) {
38
+ throw new TypeError(`predicates: variable scope must be one of ${[...SCOPES].join(', ')}`)
39
+ }
40
+ let expected
41
+ if (op === 'present' || op === 'absent') {
42
+ if (options.value !== undefined) throw new TypeError(`predicates: variable ${op} does not take value`)
43
+ } else if (op === 'in') {
44
+ if (!Array.isArray(options.value) || options.value.length === 0
45
+ || options.value.some((item) => typeof item !== 'string')) {
46
+ throw new TypeError('predicates: variable in needs a non-empty array of strings')
47
+ }
48
+ expected = options.value
49
+ } else {
50
+ if (typeof options.value !== 'string') throw new TypeError('predicates: variable value must be a string')
51
+ expected = options.value
52
+ }
53
+ const store = context.store
54
+ const moduleId = context.moduleId
55
+ const predicate = () => {
56
+ // 适配器缺失(准备期、复制协议未接)是「缺事实」,不是「变量为假」。
57
+ if (store === undefined || typeof store.read !== 'function') return UNAVAILABLE
58
+ if (typeof moduleId !== 'string' || moduleId.length === 0) return UNAVAILABLE
59
+ const state = store.read(moduleId)
60
+ if (state === null || typeof state !== 'object' || state.enabled === false) return UNAVAILABLE
61
+ const values = scope === 'global' ? state.global : scope === 'module' ? state.module : state.effective
62
+ if (values === null || typeof values !== 'object' || Array.isArray(values)) return UNAVAILABLE
63
+ const present = Object.hasOwn(values, key)
64
+ if (op === 'present') return present
65
+ if (op === 'absent') return !present
66
+ // 键缺失不是错误:比较类按假处理(作者写 present 才能区分「没有」)。
67
+ if (!present) return false
68
+ const actual = values[key]
69
+ if (typeof actual !== 'string') return false
70
+ if (op === 'eq') return actual === expected
71
+ if (op === 'ne') return actual !== expected
72
+ if (op === 'in') return expected.includes(actual)
73
+ const left = numeric(actual)
74
+ const right = numeric(expected)
75
+ if (left === undefined || right === undefined) return false
76
+ if (op === 'gt') return left > right
77
+ if (op === 'gte') return left >= right
78
+ if (op === 'lt') return left < right
79
+ return left <= right
80
+ }
81
+ predicate.kind = 'variable'
82
+ return predicate
83
+ }
@@ -0,0 +1,11 @@
1
+ /** 上下文激活诊断的只读类型声明。 */
2
+ export interface ContextActivationDiagnosticsSnapshot {
3
+ records: Array<{ id: string; stage: string; reason: string; [key: string]: unknown }>
4
+ truncated: boolean
5
+ step: number
6
+ /** false = 该会话尚未求值;true = 已求值(records 为空表示本次没有参与/入选条目)。 */
7
+ evaluated: boolean
8
+ }
9
+ export function lastContextActivationDiagnostics(session: unknown): ContextActivationDiagnosticsSnapshot
10
+ export function selectContextActivation(...args: unknown[]): Set<Record<string, unknown>> & { diagnostics?: { records: unknown[]; truncated: boolean } }
11
+ export function conversationMessages(session: unknown, pending?: unknown[]): Array<{ id: unknown; role: string; text: string }>