dsh-plugin-prompt-tool 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/README.md +100 -233
  2. package/engine/actions/append-context.mjs +3 -24
  3. package/engine/actions/assembly.mjs +70 -94
  4. package/engine/actions/catalog.mjs +65 -28
  5. package/engine/actions/content.mjs +8 -152
  6. package/engine/actions/examples.mjs +8 -4
  7. package/engine/actions/inbox-prepend.mjs +5 -7
  8. package/engine/actions/index.mjs +17 -9
  9. package/engine/actions/inject-text.mjs +24 -4
  10. package/engine/actions/pre-step-filter.mjs +3 -4
  11. package/engine/actions/registration.mjs +20 -28
  12. package/engine/actions/request-params.mjs +21 -1
  13. package/engine/actions/shared.mjs +23 -15
  14. package/engine/actions/stream-output.mjs +26 -0
  15. package/engine/actions/tool-gate.mjs +31 -0
  16. package/engine/actions/tool-result.mjs +36 -0
  17. package/engine/actions/tool-visibility.mjs +248 -0
  18. package/engine/agent-capability.mjs +67 -0
  19. package/engine/anchor-match.mjs +14 -21
  20. package/engine/audience.d.mts +2 -0
  21. package/engine/audience.mjs +102 -0
  22. package/engine/branch.d.mts +4 -0
  23. package/engine/branch.mjs +62 -19
  24. package/engine/compaction-epoch.d.mts +25 -0
  25. package/engine/compaction-epoch.mjs +27 -4
  26. package/engine/compositions/source/local/filesystem-editor.yml +3 -3
  27. package/engine/compositions/source/local/instruction-hint.yml +4 -7
  28. package/engine/compositions/source/local/module-file-tools.yml +2 -0
  29. package/engine/compositions/source/local/module-import-tools.yml +4 -0
  30. package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
  31. package/engine/compositions/source/local/module-rule-tools.yml +5 -0
  32. package/engine/compositions/source/local/module-variable-tools.yml +5 -0
  33. package/engine/compositions/source/local/present-as.yml +26 -0
  34. package/engine/compositions/source/local/tool-config-engine.yml +2 -3
  35. package/engine/compositions/source/local/tool-search.yml +11 -0
  36. package/engine/condition-facts.mjs +84 -0
  37. package/engine/conditions/anchor.mjs +63 -23
  38. package/engine/conditions/availability.mjs +1 -1
  39. package/engine/conditions/composite.mjs +4 -4
  40. package/engine/conditions/count.mjs +43 -13
  41. package/engine/conditions/evaluation.mjs +74 -19
  42. package/engine/conditions/examples.mjs +2 -1
  43. package/engine/conditions/index.d.mts +15 -0
  44. package/engine/conditions/index.mjs +23 -9
  45. package/engine/conditions/operators.mjs +4 -0
  46. package/engine/conditions/phase.mjs +10 -5
  47. package/engine/conditions/preset.mjs +1 -1
  48. package/engine/conditions/session.mjs +8 -2
  49. package/engine/conditions/subject.mjs +144 -65
  50. package/engine/conditions/text.mjs +26 -8
  51. package/engine/conditions/variable.mjs +83 -0
  52. package/engine/context-activation.d.mts +11 -0
  53. package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
  54. package/engine/delivery.d.mts +2 -0
  55. package/engine/delivery.mjs +93 -0
  56. package/engine/discovery.d.mts +38 -0
  57. package/engine/discovery.mjs +146 -0
  58. package/engine/env-safety.mjs +14 -0
  59. package/engine/executor.d.mts +28 -0
  60. package/engine/executor.mjs +290 -205
  61. package/engine/fillers.mjs +39 -8
  62. package/engine/fixed-registration.mjs +12 -0
  63. package/engine/history.mjs +71 -0
  64. package/engine/instruction-blocks.d.mts +13 -0
  65. package/engine/instruction-blocks.mjs +21 -0
  66. package/engine/instruction-files.d.mts +5 -0
  67. package/engine/instruction-files.mjs +97 -0
  68. package/engine/instruction-hint.mjs +69 -103
  69. package/engine/interpolate.d.mts +7 -0
  70. package/engine/interpolate.mjs +44 -13
  71. package/engine/layers.mjs +165 -230
  72. package/engine/machine-paths.mjs +40 -0
  73. package/engine/module-file-tools.mjs +8 -0
  74. package/engine/module-import-tools.mjs +10 -0
  75. package/engine/module-manifest-tools.mjs +8 -0
  76. package/engine/module-rule-tools.mjs +10 -0
  77. package/engine/module-variable-tools.mjs +10 -0
  78. package/engine/order.d.mts +14 -0
  79. package/engine/order.mjs +0 -3
  80. package/engine/path-safety.d.mts +2 -0
  81. package/engine/path-safety.mjs +58 -0
  82. package/engine/pre-step-source.d.mts +69 -0
  83. package/engine/pre-step-source.mjs +103 -0
  84. package/engine/present-as.mjs +77 -0
  85. package/engine/promotion-pool.d.mts +9 -0
  86. package/engine/promotion-pool.mjs +69 -0
  87. package/engine/record.mjs +2 -0
  88. package/engine/rule-engine.mjs +3 -2
  89. package/engine/rule-facts.d.mts +11 -0
  90. package/engine/rule-facts.mjs +60 -0
  91. package/engine/rule-runtime.d.mts +46 -0
  92. package/engine/rule-runtime.mjs +98 -13
  93. package/engine/rule-spec.d.mts +106 -0
  94. package/engine/rule-spec.mjs +302 -28
  95. package/engine/run-code-env.mjs +73 -19
  96. package/engine/schema.mjs +165 -243
  97. package/engine/sdk-strip.mjs +2 -2
  98. package/engine/service-names.d.mts +10 -0
  99. package/engine/service-names.mjs +10 -0
  100. package/engine/shared.d.mts +21 -0
  101. package/engine/shared.mjs +47 -26
  102. package/engine/skill-search.mjs +125 -53
  103. package/engine/source-kind.d.mts +9 -0
  104. package/engine/source-kind.mjs +16 -0
  105. package/engine/strategy-ids.mjs +5 -0
  106. package/engine/template-evaluation.d.mts +16 -0
  107. package/engine/template-evaluation.mjs +178 -0
  108. package/engine/template-frame.d.mts +16 -0
  109. package/engine/template-frame.mjs +69 -0
  110. package/engine/tool-config-engine.mjs +39 -35
  111. package/engine/tool-definition.d.mts +4 -0
  112. package/engine/tool-definition.mjs +5 -1
  113. package/engine/tool-git-bash.mjs +5 -1
  114. package/engine/tool-scope.mjs +22 -0
  115. package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
  116. package/engine/trigger.mjs +15 -270
  117. package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  118. package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
  119. package/engine/vendor/yaml/package.json +1 -1
  120. package/lib/client.js +8312 -6724
  121. package/lib/client.js.map +1 -1
  122. package/lib/index.d.mts +37 -762
  123. package/lib/index.mjs +26919 -5791
  124. package/modules/ponytail/module.yml +83 -203
  125. package/modules/ptc-mode/module.yml +45 -0
  126. package/modules/soul/assets/core-hard.md +6 -0
  127. package/modules/soul/assets/criteria.md +12 -0
  128. package/modules/soul/assets/echo.md +12 -0
  129. package/modules/soul/assets/layers.md +11 -0
  130. package/modules/soul/assets/ledger.md +9 -0
  131. package/modules/soul/assets/manifest.yml +20 -0
  132. package/modules/soul/assets/reference/00-layers.md +27 -0
  133. package/modules/soul/assets/reference/01-criteria.md +32 -0
  134. package/modules/soul/assets/reference/02-echo.md +158 -0
  135. package/modules/soul/assets/reference/03-vital.md +183 -0
  136. package/modules/soul/assets/reference/04-manifest.json +106 -0
  137. package/modules/soul/assets/reference/05-doctor.md +67 -0
  138. package/modules/soul/assets/reference/06-reference.md +189 -0
  139. package/modules/soul/assets/reference/07-bundle.json +431 -0
  140. package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
  141. package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
  142. package/modules/soul/assets/reference/10-echo-anatomy.md +100 -0
  143. package/modules/soul/assets/reference/11-self.md +80 -0
  144. package/modules/soul/assets/reference/12-ledger.md +159 -0
  145. package/modules/soul/assets/reference/README.md +10 -0
  146. package/modules/soul/assets/templates/agents-template.md +6 -0
  147. package/modules/soul/assets/vital.md +9 -0
  148. package/modules/soul/module.yml +311 -0
  149. package/modules/superpowers/module.yml +122 -0
  150. package/modules/tool-surface/module.yml +51 -47
  151. package/package.json +40 -41
  152. package/skills/dsh-module/SKILL.md +119 -0
  153. package/skills/dsh-module/reference.md +185 -0
  154. package/templates/10-pre-step.yml +2 -3
  155. package/templates/14-first-turn-anchor.yml +10 -14
  156. package/templates/15-guide-auto.yml +32 -28
  157. package/templates/16-custom-fallback.yml +33 -21
  158. package/templates/20-system-section.yml +1 -4
  159. package/templates/50-llm-stream.yml +4 -10
  160. package/templates/60-tool-pipeline.yml +12 -10
  161. package/templates/63-append-context.yml +20 -0
  162. package/templates/64-pre-step-filter.yml +23 -0
  163. package/templates/65-turn-stop.yml +2 -2
  164. package/templates/67-inbox-prepend.yml +22 -0
  165. package/templates/70-subagent-maintenance.yml +9 -10
  166. package/templates/75-subagent-end.yml +2 -2
  167. package/templates/80-tool-surface.yml +41 -57
  168. package/templates/tools/40-delegate-module-import.yml +22 -0
  169. package/templates/tools/50-delegate-module-rules.yml +14 -0
  170. package/templates/tools/60-delegate-module-variables.yml +17 -0
  171. package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
  172. package/engine/actions/decision.mjs +0 -54
  173. package/engine/actions/guard.mjs +0 -80
  174. package/engine/actions/sdk-strip.mjs +0 -30
  175. package/engine/character-tools.mjs +0 -8
  176. package/engine/classify-task.mjs +0 -53
  177. package/engine/compositions/library/agent-instructions.yml +0 -9
  178. package/engine/compositions/library/command-goal.yml +0 -7
  179. package/engine/compositions/library/compaction.yml +0 -22
  180. package/engine/compositions/library/delegation-ptc.yml +0 -58
  181. package/engine/compositions/library/delegation.yml +0 -56
  182. package/engine/compositions/library/persistent-shell.yml +0 -50
  183. package/engine/compositions/library/planning.yml +0 -26
  184. package/engine/compositions/library/skill-filesystem-cordis.yml +0 -10
  185. package/engine/compositions/library/skill-filesystem.yml +0 -7
  186. package/engine/compositions/library/tool-ask-user.yml +0 -7
  187. package/engine/compositions/library/tool-bash.yml +0 -8
  188. package/engine/compositions/library/tool-cordis.yml +0 -7
  189. package/engine/compositions/library/tool-fs-search.yml +0 -9
  190. package/engine/compositions/library/tool-fs.yml +0 -7
  191. package/engine/compositions/library/tool-goal.yml +0 -7
  192. package/engine/compositions/library/tool-jobs.yml +0 -7
  193. package/engine/compositions/library/tool-plugin-manager-disabled.yml +0 -8
  194. package/engine/compositions/library/tool-plugin-manager.yml +0 -8
  195. package/engine/compositions/library/tool-present.yml +0 -7
  196. package/engine/compositions/library/tool-presentation.yml +0 -9
  197. package/engine/compositions/library/tool-pwsh.yml +0 -8
  198. package/engine/compositions/library/tool-skill.yml +0 -7
  199. package/engine/compositions/library/tool-todo.yml +0 -9
  200. package/engine/compositions/library/tool-web.yml +0 -10
  201. package/engine/compositions/source/local/character-tools.yml +0 -5
  202. package/engine/compositions/source/local/declared-triggers.yml +0 -12
  203. package/engine/compositions/source/local/dev-tool-search.yml +0 -11
  204. package/engine/compositions/source/local/session-var-tools.yml +0 -5
  205. package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
  206. package/engine/compositions/source/local/world-book-tools.yml +0 -5
  207. package/engine/condition.mjs +0 -96
  208. package/engine/declared-triggers.mjs +0 -93
  209. package/engine/session-var-tools.mjs +0 -8
  210. package/engine/session-vars.mjs +0 -54
  211. package/engine/st-macros.d.mts +0 -22
  212. package/engine/st-macros.mjs +0 -208
  213. package/engine/st-render.mjs +0 -109
  214. package/engine/st-world-book.d.mts +0 -10
  215. package/engine/subagent-tool-policy-core.mjs +0 -400
  216. package/engine/subagent-tool-policy.mjs +0 -352
  217. package/engine/trigger-spec.mjs +0 -173
  218. package/engine/world-book-tools.mjs +0 -8
  219. package/lib/migrate-rules.d.mts +0 -1
  220. package/lib/migrate-rules.mjs +0 -39
  221. package/lib/module-package-CHPFweqK.mjs +0 -2
  222. package/lib/module-package-Cnd1_mwx.mjs +0 -2872
  223. package/lib/paths-CyP0IkaM.d.mts +0 -1390
  224. package/lib/preset-transfer.d.mts +0 -2
  225. package/lib/preset-transfer.mjs +0 -3
  226. package/lib/rules-migration-DJ6vASB5.mjs +0 -15334
  227. package/templates/policies/legacy-defaults.yml +0 -34
  228. package/templates/tools/40-delegate-character.yml +0 -15
  229. package/templates/tools/50-delegate-worldbook.yml +0 -15
  230. package/templates/tools/60-delegate-session-var.yml +0 -15
  231. package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
@@ -11,6 +11,7 @@
11
11
  * `assistant/message`, per the caller's `promoteEvents`) recorded AFTER the
12
12
  * last successful `compaction/end` boundary counts as promoted. Before any compaction
13
13
  * the boundary is -1, which preserves the original one-shot semantics.
14
+ * 重置判据看事件不看代次;理由与失效条件见 `isSuccessfulCompactionEnd`。
14
15
  *
15
16
  * State is memoized per session id and maintained incrementally through
16
17
  * `observe()`; a cold session scans its durable log once (so resume and
@@ -32,7 +33,8 @@
32
33
  * Non-gate mode keeps the original event-set semantics byte-for-byte.
33
34
  */
34
35
 
35
- import { sessionEvents, sessionState } from './shared.mjs'
36
+ import { sessionState } from './shared.mjs'
37
+ import { historyEvents } from './history.mjs'
36
38
 
37
39
  /** 匹配内容与大小写均由调用方显式声明;空 pattern 不承担任何业务识别。 */
38
40
  function reasoningClassifier(options = {}) {
@@ -69,7 +71,21 @@ function firstReasoningMatches(content, classify) {
69
71
 
70
72
  export function hasAnchoredReasoning(content, options = {}) { return firstReasoningMatches(content, reasoningClassifier(options)) }
71
73
 
72
- /** True only when compaction completed and changed the model-visible surface. */
74
+ /**
75
+ * True only when compaction completed and changed the model-visible surface.
76
+ *
77
+ * 判据刻意是**事件维**(成功 `compaction/end`),不是 surface 的 `replaceGeneration` 代次:
78
+ * 宿主对「一次已提交的位置替换」只给单调计数(`packages/core/session/src/surface.ts:569-572`),
79
+ * 不说是哪一种,也不给边界。于是代次判据有三处改语义:① 非压缩的 replace(区域编辑 / 手动裁剪 /
80
+ * 含 `surfaceOp: { op: 'replace' }` 的插件重写)同样推进代次,门控会被误重置;② 轮询只能得到
81
+ * 一个整数,定不出边界给 `applyEvent` 的 `seq <= boundary` 守卫,压缩替身落地**之前**的事件会被
82
+ * 当成新 epoch 的事件重新晋升(5b);③ 无 surface / 无代次(恢复、重放、其它宿主、既有桩)时
83
+ * 代次恒 undefined,压缩重置永不触发。压缩的**权威判定**因此还得看事件:宿主只在压缩时写
84
+ * 无 `error` 的 `compaction/end`,而成功压缩必然伴随一次 replace。
85
+ * shortcut: 近似而非权威 —— 失效条件:既不写 `compaction/end`、又靠 replace 重写可见上下文,
86
+ * 或写了成功 `compaction/end` 却没改变可见上下文时,该重置误触发/漏触发。宿主给出「这次 replace
87
+ * 是否压缩 + 边界 seq」的显式信号时改用它。
88
+ */
73
89
  export function isSuccessfulCompactionEnd(event) {
74
90
  return event?.type === 'compaction/end' && event.data?.error === undefined
75
91
  }
@@ -142,10 +158,17 @@ export function createEpochPromotion(promoteEvents, options = {}) {
142
158
  return entry
143
159
  }
144
160
 
145
- /** Scan a session's durable log from scratch (cold start / resume). */
161
+ /**
162
+ * Scan a session's durable log from scratch (cold start / resume).
163
+ *
164
+ * 判据来源**刻意是完整历史**(`historyEvents`,不是 `currentEvents`):语义是「本会话在最后
165
+ * 一次成功压缩之后**曾经** tool/call 过」——状态性的历史事实。压缩前的信号在 surface 上已
166
+ * 不可见,按当前上下文冷启动会把 `promoted` 错判为 false,门控重回 bootstrap/锚定阶段、
167
+ * 工具目录被裁剪。
168
+ */
146
169
  const scan = (session) => {
147
170
  let entry = freshEntry(-1)
148
- for (const event of sessionEvents(session)) entry = applyEvent(entry, event)
171
+ for (const event of historyEvents(session)) entry = applyEvent(entry, event)
149
172
  state.set(session, entry)
150
173
  return entry
151
174
  }
@@ -5,11 +5,11 @@
5
5
  #
6
6
  # 能力提供者边界(T4 文件归属校正):本行行下的 str-replace-editor 是**官方实现**
7
7
  # —— `@deepseek-ai/dsh-tool-str-replace-editor`(官方包,配置声明与注册都在包内),
8
- # 本仓库**没有**对应的 .mjs。它连同 subagent-tool-policy、tool-config-engine 一起
9
- # 构成 PLAN T4 的三个能力提供者,三者都不接入 engine/trigger.mjs。
8
+ # 本仓库**没有**对应的 .mjs。它连同 tool-config-engine 一起构成 PLAN T4 的两个
9
+ # 能力提供者,两者都不接入 engine/trigger.mjs。
10
10
  # 判据:会给模型提供可调用能力(注册工具/域)的 → 能力提供者;干预流程
11
11
  # (改提示词、改装配、裁决、追加)的 → 声明式触发器。
12
- # 注意 engine/tool-git-bash.mjs 提供的是 `bash` 工具,**不是**这第三个提供者。
12
+ # 注意 engine/tool-git-bash.mjs 提供的是 `bash` 工具,**不是**第二个提供者。
13
13
 
14
14
  # The bare local filesystem shadows the host's sandboxed provider only for this
15
15
  # preset. The editor shares that realm and requires absolute paths.
@@ -1,19 +1,16 @@
1
1
  # module: instruction-hint
2
2
  # source: engine/compositions/source/local/instruction-hint.yml (本地附加,非官方模块)
3
- # context-gate 的 instructionHint 转换在本模块的归属行——删除前的原始语义见
4
- # context-gate.mjs:64-66,替换点 :243-248,pre-step 挂载 :226-255:晋升后把
3
+ # 本行承接已退场的 `context-gate` 的 instructionHint 转换:晋升后把
5
4
  # agent-instructions 全文换成一次性非命令式 hint,后续全文 dump 丢弃。
6
5
  # opt-in:enabled 默认 false(与原 instructionHint 默认 false 同值),由参数桥
7
6
  # params.instructionHint(→ 本行 enabled)或本行 config.enabled: true 打开;
8
- # 相位词汇与 tool-bootstrap / promoted-code-mode 同源(compaction-epoch.mjs)。
7
+ # 相位词汇与 compaction-epoch.mjs 同源。
9
8
  #
10
9
  # 四个模板必须随行给全(2026-10-05 修):`instruction-hint.mjs#apply` 的首行是
11
10
  # `if (!enabled || templateOf(source, 'messageTemplate').trim().length === 0) return`
12
11
  # 而本行此前只配了 enabled/promoteOn/includeSubagents —— 于是把开关打开**不注册任何
13
- # 转换**:配置合法、零效果、零报错(与 count.delegated 的静默失效同类)。模板正文取自
14
- # templates/policies/legacy-defaults.yml#instructionHint,与 rules-migration.ts:102
15
- # 的迁移默认值同源;两处必须一致,否则「迁移进来的定义」与「新装的组合源」行为分叉。
16
- # legacy-defaults.yml 自身标注「引擎运行时不得读取」,所以这里必须是实体而非引用。
12
+ # 转换**:配置合法、零效果、零报错(与 count.delegated 的静默失效同类)。四个模板的正文
13
+ # 是本行自己的实体数据;旧业务默认值快照(legacy-defaults.yml)已随旧兼容移除。
17
14
  #
18
15
  # includeSubagents: true —— 子代理首次请求即视为已晋升(官方相位语义),所以子代理
19
16
  # 从第 1 轮就拿提示而非全文。主会话则是首个工具调用/助手消息之后才降级(promoteOn)。
@@ -0,0 +1,2 @@
1
+ - id: module-file-tools
2
+ name: ./engine/module-file-tools.mjs
@@ -0,0 +1,4 @@
1
+ # module: module-import-tools
2
+
3
+ - id: module-import-tools
4
+ name: ./engine/module-import-tools.mjs
@@ -0,0 +1,2 @@
1
+ - id: module-manifest-tools
2
+ name: ./engine/module-manifest-tools.mjs
@@ -0,0 +1,5 @@
1
+ # module: module-rule-tools
2
+ # source: engine/compositions/source/local/module-rule-tools.yml
3
+
4
+ - id: module-rule-tools
5
+ name: ./engine/module-rule-tools.mjs
@@ -0,0 +1,5 @@
1
+ # module: module-variable-tools
2
+ # source: engine/compositions/source/local/module-variable-tools.yml
3
+
4
+ - id: module-variable-tools
5
+ name: ./engine/module-variable-tools.mjs
@@ -0,0 +1,26 @@
1
+ # module: present-as
2
+ # source: engine/compositions/source/local/present-as.yml (本地附加,非官方模块)
3
+ # 让本模块覆盖的 Agent 以 `config.mode` 指定的形态呈现工具(官方 `ctx.tools.presentAs`):
4
+ # `ptc` 时注册表只贡献保留的 `run_code` 传输与生成的 SDK 段,其余可见能力由程序内绑定
5
+ # (`tools.xxx()`)调用。
6
+ #
7
+ # **行名必须与引擎模块文件同名**:agent-assembly 按 `engine/<行 id>.mjs` 找实现
8
+ # (src/runtime/agent-assembly.ts:186-188 的 engineModuleFile),找不到就**静默跳过这一行**
9
+ # ——官方组合行没有 engine 文件属正常(由会话原有预设提供),所以这条跳过不告警;写错名字
10
+ # 的后果是「行装了等于没装」而毫无提示。本文件因此叫 present-as.yml + id: present-as,
11
+ # 与 engine/present-as.mjs 对齐。
12
+ #
13
+ # 为什么不用官方 `@deepseek-ai/dsh-agent-tool-presentation` 行:这是插件自己的实现
14
+ # (engine/present-as.mjs 调的是同一个 `presentAs`),模块可经 `modules:` 声明它,因此
15
+ # **不需要在预设侧再挂官方行**,也不需要动 `cordis.patch.yml`(那条只用于进程级默认呈现)。
16
+ # 两者落在不同 scope——本行 = Agent scope、官方行 = 预设常驻 scope,`modeFor` 最近者优先;
17
+ # 而**同一 scope 二次声明会被官方拒绝**(挂载期 fail loud,不静默覆盖)。
18
+ #
19
+ # 前置:`mode: ptc` 需要 `ctx.ptcRuntime`(宿主 base bundle 的 `ptc-runtime` 行提供)。缺它时
20
+ # 本行停在 pending,由预设注册表指名报告不可用的行——不静默退回 native。
21
+
22
+ - id: present-as
23
+ name: ./engine/present-as.mjs
24
+ config:
25
+ enabled: true
26
+ mode: ptc
@@ -6,9 +6,8 @@
6
6
  # 能力提供者边界(T4):本行是**能力提供者**(把预设声明的自定义工具注册进官方
7
7
  # registry),**不接入** engine/trigger.mjs。判据:会给模型提供可调用能力
8
8
  # (注册工具/域)的 → 能力提供者;干预流程(改提示词、改装配、裁决、追加)的 →
9
- # 声明式触发器。登记数据在 engine/tool-config-engine.mjs 的 `engineProvider` 导出
10
- # (provides.kind = 'dynamic':工具名在装配期才由 customTools 段决定),与
11
- # `engineTriggers`(声明式触发器)互斥;守卫见 test/engine/provider-boundary.test.mjs。
9
+ # 声明式触发器。模型可见的工具名在装配期由 module.yml 的 customTools 段决定,
10
+ # 引擎侧无法静态枚举。
12
11
  # 模块内 approvalGate 与 withinCwd 是安全边界,改装配方式时不得放宽。
13
12
 
14
13
  - id: tool-config-engine
@@ -0,0 +1,11 @@
1
+ # module: tool-search
2
+ # source: engine/compositions/source/local/tool-search.yml (本地附加,非官方模块)
3
+ #
4
+ # 工具面的按需发现与解锁。与 assembly 动作的 allowFrom 配对使用:
5
+ # 本工具把解锁名写进持久调用参数(tool/call;PTC 嵌套为 tool/ptc-dispatch)→ allowExtra 读回 → 下次请求起生效。
6
+ # 只挂本行而不配 allowFrom 的话,解锁是一次性的(当次请求用完即被裁掉)。
7
+ #
8
+ # 常驻集由模板声明(见 templates/ 的工具面收窄模板),本行只负责发现入口。
9
+
10
+ - id: tool-search
11
+ name: ./engine/tool-search.mjs
@@ -0,0 +1,84 @@
1
+ /**
2
+ * condition-facts — live 规则条件的文本事实提取(if.text.subject / if.anchor.subject)。
3
+ *
4
+ * 声明式 `config.subject` / `config.match` 执行链已退役;这里只保留:
5
+ * - subject → 载荷文本字段的映射(conditions/subject 与 availability 共用);
6
+ * - 真实对话消息判据与 pre-step / turn-stop / 子代理事件的文本提取。
7
+ * 条件判定本身归 conditions/*;本模块不做命中判断、不预编译匹配器。
8
+ */
9
+
10
+ import { currentEvents } from './history.mjs'
11
+ import { extractText } from './shared.mjs'
12
+
13
+ /** 按 subject 取参与匹配的文本;live 条件 conditions/text 消费。 */
14
+ export function subjectTextOf(subject, payload) {
15
+ if (payload === undefined || payload === null) return ''
16
+ const field = SUBJECT_FIELDS[subject]
17
+ return field !== undefined && typeof payload[field] === 'string' ? payload[field] : ''
18
+ }
19
+
20
+ /** subject → 载荷字段名。改这里等于改所有层的匹配对象。 */
21
+ export const SUBJECT_FIELDS = {
22
+ toolArgs: 'argsText',
23
+ toolResult: 'resultText',
24
+ userMessage: 'userText',
25
+ assistantText: 'assistantText',
26
+ subagentInfo: 'subagentText',
27
+ }
28
+
29
+ /** 工具参数的可匹配文本:对象按 JSON 序列化,字符串原样;序列化失败按空串。 */
30
+ export function toolArgsText(args) {
31
+ if (typeof args === 'string') return args
32
+ try {
33
+ return JSON.stringify(args ?? {})
34
+ } catch {
35
+ return ''
36
+ }
37
+ }
38
+
39
+ /**
40
+ * 「真实对话消息」的唯一判据(角色过滤由消费方各自保留)。
41
+ *
42
+ * 引擎自己注入的消息同样是 user-role(`executor.mjs` 的 `role: PRE_STEP_ROLE`、
43
+ * `pluginMessage` 的 `source` 是 `plugin:<name>` 生产者身份),把它们算进来会让
44
+ * `text.match` / 关键词世界书匹配到自己注入过的正文——正文里出现「重构」「实现」
45
+ * 这类词就会自我命中,且命中判定在去重之前,`dedupe` 挡不住。
46
+ * `userMessagesText`、原生关键词世界书的扫描 haystack 与
47
+ * `context-activation.mjs#conversationMessages` 共用这一份,不再各写各的。
48
+ */
49
+ export function isConversationMessage(message) {
50
+ if (message === null || typeof message !== 'object') return false
51
+ if (message.source?.plugin !== undefined) return false
52
+ return message.source?.kind === undefined || message.source.kind === 'user'
53
+ }
54
+
55
+ /**
56
+ * pre-step 的缺省匹配对象:本批**真实对话**的用户消息文本(判据见
57
+ * {@link isConversationMessage})。
58
+ */
59
+ export function userMessagesText(messages) {
60
+ if (!Array.isArray(messages)) return ''
61
+ return messages
62
+ .filter((message) => message?.role === 'user' && isConversationMessage(message))
63
+ .map((message) => extractText(message))
64
+ .filter((text) => text.length > 0)
65
+ .join('\n')
66
+ }
67
+
68
+ /** 最后一条 assistant 消息的文本(turn-stop 的缺省匹配对象);取**当前上下文**里可见的那条。 */
69
+ export function lastAssistantText(session) {
70
+ const events = currentEvents(session)
71
+ for (let index = events.length - 1; index >= 0; index -= 1) {
72
+ if (events[index]?.type === 'assistant/message') return extractText(events[index].data)
73
+ }
74
+ return ''
75
+ }
76
+
77
+ /** 子代理事件的匹配文本:事件载荷序列化,取不到即空串。 */
78
+ export function subagentTextOf(info) {
79
+ try {
80
+ return JSON.stringify(info ?? {})
81
+ } catch {
82
+ return ''
83
+ }
84
+ }
@@ -1,31 +1,71 @@
1
- import { createAnchorMatcher } from '../anchor-match.mjs'
2
- import { sessionEvents, sessionState } from '../shared.mjs'
1
+ import { MATCH_LOGIC, createAnchorMatcher } from '../anchor-match.mjs'
2
+ import { findActionDelivery, visibleMessages } from '../delivery.mjs'
3
3
  import { sessionOf } from './subject.mjs'
4
- import { boundOf, keyList } from './values.mjs'
4
+ import { keyList, optionalBoolean } from './values.mjs'
5
+ import { UNAVAILABLE } from './availability.mjs'
5
6
 
6
- /** 首条 assistant reasoning 的确认事实;未到达时不缓存,无 id 会话不共享状态。 */
7
- export function createAnchorState(options = {}) {
8
- const matcher = createAnchorMatcher({ keys: keyList(options.keys, 'anchor.keys'), mode: 'prefix' })
9
- const confirmedBySession = sessionState(undefined, () => false)
10
- return session => {
11
- const assistants = sessionEvents(session).filter(event => event.type === 'assistant/message')
12
- let confirmed = confirmedBySession.peek(session)
13
- if (confirmed === undefined && assistants.length > 0) {
14
- const reasoning = (assistants[0].data?.message?.content ?? []).find(block => block.type === 'reasoning')
15
- confirmed = reasoning !== undefined && matcher.scan(String(reasoning.text ?? '')).active
16
- confirmedBySession.set(session, confirmed)
17
- }
18
- return { confirmed: confirmed === true, assistantRounds: assistants.length }
19
- }
7
+ const SUBJECTS = ['userMessage', 'assistantReasoning', 'assistantText', 'toolResult']
8
+ const FIELDS = new Set(['subject', 'keys', 'secondaryKeys', 'caseSensitive', 'wholeWords', 'useRegex', 'logic', 'mode', 'wordBoundary', 'fallbackAfter', 'after'])
9
+
10
+ /** 按所选来源取正文;缺少该来源的内容块不算一次相应输出。 */
11
+ function outputText(subject, event, message) {
12
+ if (subject === 'userMessage') {
13
+ if (event.type !== 'user/message' || message.role !== 'user' || message.source?.kind !== 'user' || message.source?.plugin !== undefined) return undefined
14
+ } else if (subject === 'toolResult') {
15
+ if (event.type !== 'tool/result') return undefined
16
+ } else if (event.type !== 'assistant/message' || event.data?.interrupted === true) return undefined
17
+ const type = subject === 'assistantReasoning' ? 'reasoning' : 'text'
18
+ const blocks = (Array.isArray(message.content) ? message.content : []).filter(block => block?.type === type)
19
+ return blocks.length === 0 ? undefined : blocks.map(block => typeof block.text === 'string' ? block.text : '').join('\n')
20
20
  }
21
21
 
22
- /** 确认即命中;可选 fallbackAfter 表示超过指定 assistant 消息数后也允许。 */
23
- export function createAnchorPredicate(options = {}) {
24
- const inspect = createAnchorState(options)
25
- const fallbackAfter = boundOf(options.fallbackAfter, 'anchor.fallbackAfter')
22
+ /** 从当前可见、已结算消息重建确认;关联只从目标实际投递的日志序号之后开始。 */
23
+ export function createAnchorPredicate(options = {}, context = {}) {
24
+ for (const key of Object.keys(options)) if (!FIELDS.has(key)) throw new TypeError(`anchor: unknown field ${key}`)
25
+ const subject = options.subject ?? 'assistantReasoning'
26
+ if (!SUBJECTS.includes(subject)) throw new TypeError(`anchor.subject must be one of ${SUBJECTS.join(', ')}`)
27
+ const mode = options.mode ?? 'prefix'
28
+ if (!['prefix', 'scan'].includes(mode)) throw new TypeError('anchor.mode must be prefix or scan')
29
+ if (mode === 'prefix' && ['secondaryKeys', 'caseSensitive', 'wholeWords', 'useRegex', 'logic', 'wordBoundary'].some(key => options[key] !== undefined)) {
30
+ throw new TypeError('anchor prefix matches literal keys only; use scan for other matching options')
31
+ }
32
+ if (options.logic !== undefined && !Object.values(MATCH_LOGIC).includes(options.logic)) throw new TypeError('anchor.logic is invalid')
33
+ const fallbackAfter = options.fallbackAfter
34
+ if (fallbackAfter !== undefined && (!Number.isSafeInteger(fallbackAfter) || fallbackAfter <= 0)) throw new TypeError('anchor.fallbackAfter must be a positive integer')
35
+ if (options.after !== undefined && (options.after === null || typeof options.after !== 'object' || Array.isArray(options.after)
36
+ || Object.keys(options.after).some(key => key !== 'action') || typeof options.after.action !== 'string' || options.after.action.trim().length === 0)) {
37
+ throw new TypeError('anchor.after must be { action: non-empty string }')
38
+ }
39
+ const matcher = createAnchorMatcher({
40
+ keys: keyList(options.keys, 'anchor.keys'),
41
+ secondaryKeys: keyList(options.secondaryKeys, 'anchor.secondaryKeys'),
42
+ caseSensitive: optionalBoolean(options.caseSensitive, 'anchor.caseSensitive', false),
43
+ wholeWords: optionalBoolean(options.wholeWords, 'anchor.wholeWords', false),
44
+ useRegex: optionalBoolean(options.useRegex, 'anchor.useRegex', undefined),
45
+ logic: options.logic,
46
+ wordBoundary: options.wordBoundary,
47
+ mode,
48
+ })
26
49
  const predicate = payload => {
27
- const status = inspect(sessionOf(payload))
28
- return status.confirmed || (fallbackAfter !== undefined && status.assistantRounds > fallbackAfter)
50
+ const session = sessionOf(payload)
51
+ if (typeof session?.snapshotEvents !== 'function') return UNAVAILABLE
52
+ const messages = visibleMessages(session)
53
+ let start
54
+ if (options.after !== undefined) {
55
+ const target = context.resolveAction?.(options.after.action)
56
+ if (target === undefined) return UNAVAILABLE
57
+ start = findActionDelivery(session, target, messages)
58
+ if (start === undefined) return UNAVAILABLE
59
+ }
60
+ let outputs = 0
61
+ for (const { event, seq, message } of messages) {
62
+ if (start !== undefined && seq <= start.seq) continue
63
+ const text = outputText(subject, event, message)
64
+ if (text === undefined) continue
65
+ if (matcher.scan(text).active) return true
66
+ if (fallbackAfter !== undefined && ++outputs >= fallbackAfter) return true
67
+ }
68
+ return false
29
69
  }
30
70
  predicate.kind = 'anchor'
31
71
  return predicate
@@ -1,5 +1,5 @@
1
1
  import { agentOf, sessionOf } from './subject.mjs'
2
- import { SUBJECT_FIELDS } from '../condition.mjs'
2
+ import { SUBJECT_FIELDS } from '../condition-facts.mjs'
3
3
 
4
4
  /** 缺少真实事件事实不是 false;尤其不能经 not 反转成允许。 */
5
5
  export const UNAVAILABLE = Symbol('condition-input-unavailable')
@@ -1,6 +1,6 @@
1
1
  import { MATCH_LOGIC } from '../anchor-match.mjs'
2
2
  import { UNAVAILABLE } from './availability.mjs'
3
- const COMPOSITE_OPERATORS = Object.values(MATCH_LOGIC)
3
+ import { COMPOSITE_OPERATORS } from './operators.mjs'
4
4
 
5
5
  export function composite(node) {
6
6
  if (typeof node === 'function') return node
@@ -69,10 +69,10 @@ export function composite(node) {
69
69
  predicate.kind = 'composite'
70
70
  // 组合层必须**转发子谓词的 observe**(B7 T1 的跨能力缺口修复):两条挂载路径的观察器
71
71
  // 接线都按 `when.observe` 过滤(`trigger.mjs` 的 `wireTriggerObservers`),被组合包住的
72
- // phase / count / session 谓词否则永远收不到活的 `session/event`——状态按 `session.id`
73
- // 缓存后不再重扫(`shared.mjs` 的 `sessionMapGet`),判定会停在冷扫那一刻(计数恒旧值、
72
+ // phase / count 谓词否则永远收不到活的 `session/event`——状态按 `session.id`
73
+ // 缓存后不再重扫(`shared.mjs` 的 `sessionState`),判定会停在冷扫那一刻(计数恒旧值、
74
74
  // 相位永不晋升)。`phase` 有 `subscribe: false` 可绕(代价是每次判定冷扫),
75
- // `count` / `session` 没有该档,所以这条转发是它们唯一的活路。
75
+ // `count` 没有该档,所以这条转发是它唯一的活路。
76
76
  const observers = children.filter((child) => typeof child.observe === 'function')
77
77
  if (observers.length > 0) {
78
78
  predicate.observe = (session, event) => {
@@ -1,4 +1,6 @@
1
- import { MAX_TRACKED_SESSIONS, extractText, isDelegated, sessionEvents } from '../shared.mjs'
1
+ import { MAX_TRACKED_SESSIONS, extractText, isDelegated } from '../shared.mjs'
2
+ import { SURFACE_MESSAGE_TYPES, currentEvents, historyEvents, viewRestricted } from '../history.mjs'
3
+ import { isSuccessfulCompactionEnd } from '../compaction-epoch.mjs'
2
4
  import { sessionOf } from './subject.mjs'
3
5
  import { boundOf, optionalBoolean } from './values.mjs'
4
6
 
@@ -20,12 +22,18 @@ const TURN_EVENT = 'turn/start'
20
22
  /**
21
23
  * 计数判断:次数 / 字符数 / 轮次,带上下限与**冷启动重建**。
22
24
  *
23
- * - `per: 'session'` 计整个 durable log;`per: 'turn'` 只计**最新轮**,当前轮由
25
+ * - `per: 'session'` 计本会话全部(视图口径见下条);`per: 'turn'` 只计**最新轮**,当前轮由
24
26
  * `turn/start` 与计数信号自身带轮号的事件共同推进(与 deliberation-gate 的
25
27
  * `turnOf` 同规则),轮号取自 `event.data.turn`;无轮号的事件不参与轮内计数
26
- * (宁可少计,也不把跨轮数据算进当前轮)。
27
- * - 冷启动:首次判定冷扫 `sessionEvents(session)` 重建(重启/恢复/重挂后同一结果),
28
- * 之后 O(1);`observe` 是增量喂入,**只喂已判定过的会话**,避免「冷扫 + 增量」重复计数。
28
+ * (宁可少计,也不把跨轮数据算进当前轮)。消息类信号读当前上下文时看不到 `turn/start`,
29
+ * 当前轮只由带轮号的可见信号推进。
30
+ * - 视图:消息类信号读**当前上下文**——被压缩 / 位置替换遮蔽的消息不再计入;`tool/call` 与
31
+ * `turn/start` 不是 surface 承载类型,读**完整历史**,「重启/恢复/重挂后同一结果」由此保住。
32
+ * - 冷启动:首次判定冷扫重建(重启/恢复/重挂后同一结果),之后 O(1);`observe` 是增量喂入,
33
+ * **只喂已判定过的会话**,避免「冷扫 + 增量」重复计数。成功压缩或 surface 代次推进时条目
34
+ * 整体作废、下次求值重建——这条复位只对**消息类**信号生效(它读可见上下文),log-only
35
+ * 信号重建读完整历史、压缩不改变它;压缩后 `every: N` 的节奏随之重置,可能立刻再命中一次
36
+ * (已接受的用户可见行为,见 CHANGELOG)。
29
37
  * - 上下限:`count >= min`(声明时)且 `count <= max`(声明时)。至少要声明一侧,
30
38
  * 否则该判定恒真——那是把配置错误伪装成命中。
31
39
  * - 取不到会话 = 无事件 = 计数 0(与空会话同一语义,不发明数据);无 `id` 的会话
@@ -50,8 +58,7 @@ export function createCountPredicate(options = {}) {
50
58
  const min = boundOf(options.min, 'min')
51
59
  const max = boundOf(options.max, 'max')
52
60
  // 节奏判据(2026-09-22 用户拍板⑤「改用可重建计数」的核心语义):`every: N` = 每 N 次命中一次。
53
- // 原 `progress-reminder` 的 `results % every === 0` 配的是**自增后**的计数
54
- // (声明侧见 `test/engine/declarations/progress-reminder.yml` 的 `count.every`),
61
+ // 原 `progress-reminder` 的 `results % every === 0` 配的是**自增后**的计数,
55
62
  // 所以这里必须要求 `count > 0`——否则 `0 % N === 0` 会让第 0 次就命中。
56
63
  const every = options.every === undefined
57
64
  ? undefined
@@ -74,9 +81,12 @@ export function createCountPredicate(options = {}) {
74
81
  // **主判据就是 count**,受众不能只挂在 `session` 谓词上——那会迫使声明额外耦合一个
75
82
  // durable 事件类型(还要回答「哪种 type 恒真」,而哨兵轮表明甚至可能没有轮事件)。
76
83
  const delegated = optionalBoolean(options.delegated, 'delegated', undefined)
77
- /** sessionId -> { total, turns, lastTurn }(进程内快路径,真相在 durable 事件流)。 */
84
+ /** sessionId -> { total, turns, lastTurn, generation }(进程内快路径,真相在 durable 事件流)。 */
78
85
  const state = new Map()
79
- const fresh = () => ({ total: 0, turns: new Map(), lastTurn: undefined })
86
+ const fresh = (generation) => ({ total: 0, turns: new Map(), lastTurn: undefined, generation })
87
+ const generationOf = (session) => session?.surface?.replaceGeneration
88
+ /** 消息类信号按当前上下文计,log-only 信号按完整历史计(判据见 `SURFACE_MESSAGE_TYPES`)。 */
89
+ const view = SURFACE_MESSAGE_TYPES.has(signal.type) ? currentEvents : historyEvents
80
90
  const measure = (event) => (signal.measure === 'chars' ? extractText(event.data).length : 1)
81
91
  /** 轮号推进:与 deliberation-gate 的 turnOf/turnEntryOf 同规则——`turn/start` 与
82
92
  * 计数信号自身带轮号的事件都推进当前轮,且取最大值;新轮先落 0,再累加。 */
@@ -112,14 +122,24 @@ export function createCountPredicate(options = {}) {
112
122
  ? entry.total
113
123
  : (entry.lastTurn === undefined ? 0 : entry.turns.get(entry.lastTurn) ?? 0))
114
124
  const rebuild = (session) => {
115
- const entry = fresh()
116
- for (const event of sessionEvents(session)) applyEvent(entry, event)
125
+ const entry = fresh(generationOf(session))
126
+ // 位置替换后同一事件可在 `surface.nodes` 里占多个位置(`history.mjs` 头部):按条数直数会虚高,
127
+ // `every: N` 提前触发。增量 `observe` 一次只收一条新事件,无需去重。
128
+ const seen = new Set()
129
+ for (const event of view(session)) {
130
+ if (event?.seq !== undefined) {
131
+ if (seen.has(event.seq)) continue
132
+ seen.add(event.seq)
133
+ }
134
+ applyEvent(entry, event)
135
+ }
117
136
  return entry
118
137
  }
119
138
  const entryOf = (session) => {
120
139
  if (session?.id === undefined) return rebuild(session)
121
140
  const known = state.get(session.id)
122
- if (known !== undefined) return known
141
+ // 位置替换没有对应的 durable 事件可观察,只在 surface 代次上前进:代次变了就按当前上下文重建。
142
+ if (known !== undefined && known.generation === generationOf(session)) return known
123
143
  const entry = rebuild(session)
124
144
  if (state.size >= MAX_TRACKED_SESSIONS) state.clear()
125
145
  state.set(session.id, entry)
@@ -133,7 +153,17 @@ export function createCountPredicate(options = {}) {
133
153
  }
134
154
  predicate.kind = 'count'
135
155
  predicate.observe = (session, event) => {
136
- const entry = session?.id === undefined ? undefined : state.get(session.id)
156
+ if (session?.id === undefined) return
157
+ // 失效条件必须与它读的视图一致:只有消息类信号按可见上下文计数,压缩复位对
158
+ // log-only 信号(重建读 `historyEvents`)不改变任何结果,白白触发一次重建。
159
+ const flagged = SURFACE_MESSAGE_TYPES.has(signal.type)
160
+ // 成功压缩换掉了可见历史,而条目按**全部** durable 事件累加过(含被遮蔽的),无法反演
161
+ // → 整体清条目、下次求值重建(宿主没暴露 `replaceGeneration` 时这是唯一的复位信号)。
162
+ if (flagged && isSuccessfulCompactionEnd(event)) { state.delete(session.id); return }
163
+ // 视图受限时重建路径读 `currentEvents`,那里没有 `turn/start` 这类 log-only 事件;
164
+ // 增量若照单全收就会靠它推进当前轮,同一会话两种结论(「冷启动 == 增量」失效)。
165
+ if (flagged && viewRestricted(session) && !SURFACE_MESSAGE_TYPES.has(event?.type)) return
166
+ const entry = state.get(session.id)
137
167
  if (entry !== undefined) applyEvent(entry, event)
138
168
  }
139
169
  return predicate
@@ -1,24 +1,59 @@
1
1
  import { subjectOf } from './subject.mjs'
2
+ import { UNAVAILABLE } from './availability.mjs'
2
3
 
3
4
  /**
4
5
  * 一次官方调用的局部判定帧;不按 session、turn 或载荷身份缓存。
5
- * `ctx` 是判定期服务入口:子代理事件靠它用 `id` 反查 agent(见 subject.mjs 的
6
- * `subagentFacts`),使 `modelScope`/`audience` 在判定阶段就有事实可用。
6
+ * `ctx` 是判定期服务入口:子代理事件靠它用 `id` 反查 agent(见 `subject.mjs` 的通道事实表
7
+ * `CHANNELS`),使 `modelScope`/`audience` 在判定阶段就有事实可用。
8
+ * `onOutcome` 是可选只读上报(规则、通道、判定类别),供宿主统计「配了没生效」;
9
+ * 缺省不传即零开销,也不改变任何判定结果。
7
10
  */
8
- export function ruleFrame(channel, args, warnOnce = () => {}, ctx) {
9
- return { channel, args, subject: subjectOf(channel, args, warnOnce, ctx), decisions: new Map(), warnOnce }
11
+ export function ruleFrame(channel, args, warnOnce = () => {}, ctx, onOutcome) {
12
+ return { channel, args, subject: subjectOf(channel, args, warnOnce, ctx), decisions: new Map(), actionDecisions: new Set(), warnOnce, onOutcome }
10
13
  }
11
14
 
12
- export function ruleMatches(rule, frame) {
13
- if (rule === undefined) return true
14
- if (frame.decisions.has(rule)) return frame.decisions.get(rule)
15
+ /** 条件返回值的三值归一:词表与 bridge 的 `RuleOutcomeKind` 共享,故只此一处定义。 */
16
+ function classifyOutcome(decided) {
17
+ return decided === true ? 'hit' : decided === UNAVAILABLE ? 'unavailable' : 'miss'
18
+ }
19
+
20
+ /** 只读上报:诊断抛错不得反过来打断判定(调用方可能在记账里访问规则身份)。 */
21
+ function report(frame, rule, outcome) {
22
+ try { frame.onOutcome?.(rule, frame.channel, outcome) } catch { /* 诊断不得反过来打断判定 */ }
23
+ }
24
+
25
+ /** 单次规则级判定(帧内缓存,键为规则实例):三值语义见下;只判定、不上报。 */
26
+ function decideRule(rule, frame) {
27
+ if (rule === undefined) return { hit: true, outcome: 'hit' }
28
+ const cached = frame.decisions.get(rule)
29
+ if (cached !== undefined) return cached
15
30
  let hit = rule.enabled !== false
31
+ let outcome = hit ? 'hit' : 'disabled'
16
32
  if (hit && typeof rule.when === 'function') {
17
- try { hit = rule.when(frame.subject) === true }
18
- catch (error) { hit = false; frame.warnOnce(`rule ${rule.id} condition failed: ${String(error?.message ?? error)}`) }
33
+ try {
34
+ const decided = rule.when(frame.subject)
35
+ hit = decided === true
36
+ // 三值语义:缺事实(UNAVAILABLE)与「条件为假」在诊断里必须分开——前者是
37
+ // 「为什么没触发」的答案,后者是条件按设计不满足。
38
+ outcome = classifyOutcome(decided)
39
+ } catch (error) {
40
+ hit = false
41
+ outcome = 'error'
42
+ frame.warnOnce(`rule ${rule.id} condition failed: ${String(error?.message ?? error)}`)
43
+ }
19
44
  }
20
- frame.decisions.set(rule, hit)
21
- return hit
45
+ const decided = { hit, outcome }
46
+ frame.decisions.set(rule, decided)
47
+ return decided
48
+ }
49
+
50
+ export function ruleMatches(rule, frame) {
51
+ if (rule === undefined) return true
52
+ const cached = frame.decisions.get(rule)
53
+ if (cached !== undefined) return cached.hit
54
+ const decided = decideRule(rule, frame)
55
+ report(frame, rule, decided.outcome)
56
+ return decided.hit
22
57
  }
23
58
 
24
59
  /**
@@ -27,15 +62,35 @@ export function ruleMatches(rule, frame) {
27
62
  * `bypassRuleWhen` 的动作来自规则级 `else`——它的条件里已经含 `not(if)`,再叠加
28
63
  * `rule.when` 会自相矛盾、令该分支永不执行。动作条件缺省 = 恒真;非 `true`
29
64
  * (含 UNAVAILABLE 三值语义)一律不命中,与 `ruleMatches` 同一纪律。
65
+ *
66
+ * 上报(方案 A:不加 actionId 维度):带动作级分支的动作上报「规则∧动作」的**最终**
67
+ * 判定,按帧对每个动作实例去重一次——分支为真记 `hit`、为假记 `miss`、缺事实记
68
+ * `unavailable`、异常记 `error`;这类动作不再单独发规则级上报,避免同一事件先记
69
+ * `hit` 再记 `miss`。没有分支的动作最终判定就是规则级判定,沿用规则级上报。
30
70
  */
31
71
  export function actionMatches(entry, frame) {
32
- if (entry.bypassRuleWhen !== true && !ruleMatches(entry.rule, frame)) return false
33
- const when = entry.actionWhen
34
- if (when === undefined) return true
35
- try {
36
- return when(frame.subject) === true
37
- } catch (error) {
38
- frame.warnOnce(`action ${entry.id} condition failed: ${String(error?.message ?? error)}`)
39
- return false
72
+ const branched = entry.actionWhen !== undefined || entry.bypassRuleWhen === true
73
+ if (!branched) return ruleMatches(entry.rule, frame)
74
+ // `else`(bypassRuleWhen)自带 not(if),不叠加规则级判定;其余带分支的动作自判
75
+ // 规则级(不发上报),判定仍走同一份帧内缓存,同帧同规则依旧只判一次。
76
+ const ruled = entry.bypassRuleWhen === true ? { hit: true, outcome: 'hit' } : decideRule(entry.rule, frame)
77
+ let hit = ruled.hit
78
+ let outcome = ruled.outcome
79
+ if (hit) {
80
+ try {
81
+ const decided = entry.actionWhen === undefined ? true : entry.actionWhen(frame.subject)
82
+ hit = decided === true
83
+ outcome = classifyOutcome(decided)
84
+ } catch (error) {
85
+ hit = false
86
+ outcome = 'error'
87
+ frame.warnOnce(`action ${entry.id} condition failed: ${String(error?.message ?? error)}`)
88
+ }
89
+ }
90
+ // 只去重上报,不缓存判定结果:同帧内重复求值仍按当次事实判定。
91
+ if (!frame.actionDecisions.has(entry)) {
92
+ frame.actionDecisions.add(entry)
93
+ report(frame, entry.rule, outcome)
40
94
  }
95
+ return hit
41
96
  }
@@ -7,5 +7,6 @@ export const PREDICATE_EXAMPLES = {
7
7
  session: { type: 'user/message', present: false },
8
8
  preset: { presetId: 'preset-id' },
9
9
  scope: { audience: 'main', modelScope: 'all' },
10
- anchor: { keys: ['We'], fallbackAfter: 1 },
10
+ anchor: { subject: 'assistantReasoning', keys: ['确认'], mode: 'prefix' },
11
+ variable: { key: 'quota', op: 'lte', value: '0', scope: 'effective' },
11
12
  }