dsh-plugin-prompt-tool 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/README.md +185 -46
  2. package/engine/THIRD_PARTY_LICENSES +25 -0
  3. package/engine/actions.mjs +931 -0
  4. package/engine/anchor-match.mjs +6 -3
  5. package/engine/character-tools.mjs +11 -0
  6. package/engine/classify-task.mjs +18 -0
  7. package/engine/compaction-epoch.mjs +41 -20
  8. package/engine/compositions/library/agent-instructions.yml +9 -0
  9. package/engine/compositions/library/command-goal.yml +2 -6
  10. package/engine/compositions/library/compaction.yml +2 -32
  11. package/engine/compositions/library/delegation-ptc.yml +58 -0
  12. package/engine/compositions/library/delegation.yml +5 -38
  13. package/engine/compositions/library/persistent-shell.yml +4 -13
  14. package/engine/compositions/library/planning.yml +8 -23
  15. package/engine/compositions/library/skill-filesystem-cordis.yml +10 -0
  16. package/engine/compositions/library/skill-filesystem.yml +2 -8
  17. package/engine/compositions/library/tool-ask-user.yml +2 -3
  18. package/engine/compositions/library/tool-bash.yml +4 -12
  19. package/engine/compositions/library/tool-cordis.yml +7 -0
  20. package/engine/compositions/library/tool-fs-search.yml +2 -11
  21. package/engine/compositions/library/tool-fs.yml +2 -5
  22. package/engine/compositions/library/tool-goal.yml +2 -6
  23. package/engine/compositions/library/tool-jobs.yml +2 -18
  24. package/engine/compositions/library/tool-plugin-manager-disabled.yml +8 -0
  25. package/engine/compositions/library/tool-plugin-manager.yml +8 -0
  26. package/engine/compositions/library/tool-present.yml +7 -0
  27. package/engine/compositions/library/tool-presentation.yml +9 -0
  28. package/engine/compositions/library/tool-pwsh.yml +2 -6
  29. package/engine/compositions/library/tool-skill.yml +7 -0
  30. package/engine/compositions/library/tool-todo.yml +2 -4
  31. package/engine/compositions/library/tool-web.yml +2 -1
  32. package/engine/compositions/source/local/character-tools.yml +5 -0
  33. package/engine/compositions/source/local/declared-triggers.yml +12 -0
  34. package/engine/compositions/source/local/filesystem-editor.yml +30 -0
  35. package/engine/compositions/source/local/instruction-hint.yml +15 -0
  36. package/engine/compositions/{library/official-persistent-shell.yml → source/local/persistent-shell-posix.yml} +10 -8
  37. package/engine/compositions/source/local/prompt-config-engine.yml +3 -9
  38. package/engine/compositions/source/local/run-code-env.yml +4 -7
  39. package/engine/compositions/source/local/session-var-tools.yml +5 -0
  40. package/engine/compositions/source/local/skill-search.yml +3 -9
  41. package/engine/compositions/source/local/subagent-tool-policy.yml +21 -0
  42. package/engine/compositions/source/local/tool-bash-disabled.yml +9 -0
  43. package/engine/compositions/source/local/tool-config-engine.yml +8 -0
  44. package/engine/compositions/source/local/tool-git-bash.yml +15 -0
  45. package/engine/compositions/source/local/world-book-tools.yml +5 -0
  46. package/engine/condition.mjs +83 -0
  47. package/engine/declared-triggers.mjs +93 -0
  48. package/engine/executor.mjs +364 -185
  49. package/engine/fields.mjs +219 -0
  50. package/engine/fillers.mjs +12 -141
  51. package/engine/host-package.mjs +51 -0
  52. package/engine/instruction-hint.mjs +285 -0
  53. package/engine/interpolate.mjs +157 -29
  54. package/engine/layers.mjs +495 -75
  55. package/engine/predicates.mjs +753 -0
  56. package/engine/prompt-config-engine.mjs +63 -8
  57. package/engine/run-code-env.mjs +48 -31
  58. package/engine/schema.mjs +444 -103
  59. package/engine/sdk-strip.mjs +365 -0
  60. package/engine/session-var-tools.mjs +11 -0
  61. package/engine/session-vars.mjs +8 -6
  62. package/engine/shared.mjs +137 -0
  63. package/engine/skill-search.mjs +34 -21
  64. package/engine/st-macros.d.mts +22 -0
  65. package/engine/st-macros.mjs +208 -0
  66. package/engine/st-render.mjs +105 -0
  67. package/engine/st-world-book.d.mts +10 -0
  68. package/engine/st-world-book.mjs +259 -0
  69. package/engine/strategies.mjs +31 -20
  70. package/engine/subagent-tool-policy-core.mjs +400 -0
  71. package/engine/subagent-tool-policy.mjs +327 -0
  72. package/engine/tool-config-engine.mjs +207 -145
  73. package/engine/tool-definition.mjs +143 -0
  74. package/engine/{custom-bash.mjs → tool-git-bash.mjs} +38 -13
  75. package/engine/trigger-editor-meta.mjs +48 -0
  76. package/engine/trigger-spec.mjs +257 -0
  77. package/engine/trigger.mjs +304 -0
  78. package/engine/world-book-tools.mjs +11 -0
  79. package/lib/client.js +16495 -5456
  80. package/lib/client.js.map +1 -1
  81. package/lib/index.d.mts +628 -382
  82. package/lib/index.mjs +5391 -2942
  83. package/lib/paths-CY5psBqE.d.mts +1233 -0
  84. package/lib/preset-package-BnJqrU4R.mjs +16792 -0
  85. package/lib/preset-transfer.d.mts +2 -0
  86. package/lib/preset-transfer.mjs +2 -0
  87. package/package.json +91 -49
  88. package/preset/pt-cordis/preset.yml +43 -0
  89. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +29 -0
  90. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +501 -0
  91. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +38 -0
  92. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +60 -0
  93. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +5 -0
  94. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +13 -0
  95. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +5 -0
  96. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +22 -0
  97. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +3 -0
  98. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +2 -0
  99. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +15 -0
  100. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +8 -0
  101. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +7 -0
  102. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +84 -0
  103. package/preset/pt-custom/preset.yml +15 -0
  104. package/preset/pt-minimal/preset.yml +27 -0
  105. package/preset/pt-ptc/preset.yml +45 -0
  106. package/preset/pt-standard/preset.yml +43 -0
  107. package/templates/14-first-turn-anchor.yml +4 -4
  108. package/templates/15-guide-auto.yml +3 -3
  109. package/templates/16-custom-fallback.yml +1 -0
  110. package/templates/18-placeholder-env-facts.yml +3 -3
  111. package/templates/20-system-section.yml +3 -2
  112. package/templates/65-turn-stop.yml +18 -0
  113. package/templates/66-subagent-start.yml +14 -0
  114. package/templates/75-subagent-end.yml +15 -0
  115. package/templates/tools/10-shell-command.yml +1 -1
  116. package/templates/tools/40-delegate-character.yml +1 -1
  117. package/templates/tools/50-delegate-worldbook.yml +1 -1
  118. package/templates/tools/60-delegate-session-var.yml +1 -1
  119. package/templates/tools/70-delegate-worldbook-upsert.yml +1 -1
  120. package/engine/anchor-turn.mjs +0 -68
  121. package/engine/code-presentation.mjs +0 -103
  122. package/engine/compositions/library/anchor-turn.yml +0 -10
  123. package/engine/compositions/library/bootstrap-filesystem.yml +0 -19
  124. package/engine/compositions/library/code-presentation.yml +0 -8
  125. package/engine/compositions/library/context-gate.yml +0 -39
  126. package/engine/compositions/library/cot-drip.yml +0 -11
  127. package/engine/compositions/library/custom-bash.yml +0 -14
  128. package/engine/compositions/library/deliberation-gate.yml +0 -11
  129. package/engine/compositions/library/official-agent-instructions.yml +0 -17
  130. package/engine/compositions/library/official-skill-filesystem-cordis.yml +0 -10
  131. package/engine/compositions/library/official-tool-bash.yml +0 -6
  132. package/engine/compositions/library/official-tool-cordis.yml +0 -13
  133. package/engine/compositions/library/official-tool-presentation.yml +0 -7
  134. package/engine/compositions/library/official-tool-skill.yml +0 -14
  135. package/engine/compositions/library/persona.yml +0 -13
  136. package/engine/compositions/library/prompt-config-engine.yml +0 -16
  137. package/engine/compositions/library/run-code-env.yml +0 -15
  138. package/engine/compositions/library/skill-search.yml +0 -14
  139. package/engine/compositions/library/str-replace-editor.yml +0 -10
  140. package/engine/compositions/library/tool-bootstrap.yml +0 -14
  141. package/engine/compositions/library/tool-config-engine.yml +0 -9
  142. package/engine/compositions/library/tool-filter.yml +0 -8
  143. package/engine/compositions/source/local/code-presentation.yml +0 -8
  144. package/engine/compositions/source/local/context-gate.yml +0 -36
  145. package/engine/compositions/source/local/custom-bash.yml +0 -11
  146. package/engine/compositions/source/local/str-replace-editor.yml +0 -10
  147. package/engine/compositions/source/local/tool-bootstrap.yml +0 -11
  148. package/engine/compositions/source/local/tool-filter.yml +0 -8
  149. package/engine/context-gate.mjs +0 -305
  150. package/engine/cot-drip.mjs +0 -137
  151. package/engine/deliberation-gate.mjs +0 -127
  152. package/engine/tool-bootstrap.mjs +0 -476
  153. package/engine/tool-filter.mjs +0 -80
  154. package/lib/preset-core.d.mts +0 -10
  155. package/lib/preset-core.mjs +0 -77
  156. package/lib/prompt-configs-BnQKRHVb.mjs +0 -884
  157. package/lib/prompt-configs-CN0sJTUL.d.mts +0 -193
  158. package/preset/anchored/preset.yml +0 -296
  159. package/preset/creative/preset.yml +0 -67
  160. package/preset/creative/skills/cordis-plugin-development/SKILL.md +0 -420
  161. package/preset/creative/skills/editing-cordis-compositions/SKILL.md +0 -165
  162. package/preset/custom/preset.yml +0 -12
  163. package/preset/liangshen/preset.yml +0 -75
  164. package/preset/minimal/preset.yml +0 -45
  165. package/preset/ptc/preset.yml +0 -58
  166. package/preset/standard/preset.yml +0 -57
  167. package/skills/manifest.json +0 -7
  168. package/skills/sandboxmod/SKILL.md +0 -49
  169. package/skills/web ui/SKILL.md +0 -42
@@ -0,0 +1,304 @@
1
+ /**
2
+ * trigger — 触发器引擎(B3):把「声明」变成「注册」。
3
+ *
4
+ * 本文件负责调度层,**不含**判断原语与动作实现:
5
+ * - `when` 由 `engine/predicates.mjs` 提供(七类判断的组合);
6
+ * - `do` 由 `engine/actions.mjs` 提供(七类动作);
7
+ * - 本文件只做:声明校验(复用 B2 的 fields.mjs)、同通道内的稳定排序、
8
+ * 宿主 waterfall 位置映射、注册与 disposer、降级与统一告警、会话态生命周期。
9
+ *
10
+ * ## 为什么排序必须由运行时自己做(R13 的权威依据)
11
+ *
12
+ * 官方 Cordis(`node_modules/@deepseek-ai/cordis/src/events.ts`)**没有数字排序**:
13
+ * - `:254-260` `register()` 只做 `options.prepend ? 'unshift' : 'push'`;
14
+ * - `:234-243` `waterfall()` 用 `cbs.shift()` 从链首取,所以 unshift 的监听器最先执行
15
+ * (最外层),而**多个 prepend 之间后注册的更靠前**;
16
+ * - `:228-229` 注释:不调用 `next()` 的监听器会否决其后整条链(含内置行为)。
17
+ *
18
+ * 因此调度必须拆成两个互不混淆的字段,**不得**用一个字段同时承担两件事:
19
+ * - `channelOrder: number` —— **同一官方通道内**的声明顺序,本文件做有界稳定排序
20
+ * (升序,同值保持声明序),并标明动作在 `next()` 前还是后;
21
+ * - `waterfallPosition` —— 适配器在宿主 waterfall 中的位置,只有 `'outermost'`
22
+ * 才映射为 `prepend: true`,**不承担**声明间排序。
23
+ *
24
+ * **跨九层的全局调度禁止**:官方九层插入点彼此独立,`prepend` 也不得替代执行层 guard
25
+ * 的单调边界(那是 `ctx.tools.guard()` 的职责,见 B3 T2 的动作 (5))。
26
+ *
27
+ * ## 声明载体约定(与 T4 的能力提供者边界守卫对齐)
28
+ *
29
+ * 本文件导出的是**引擎本身**(`mountTriggers` / `orderTriggers` / `validateTrigger` /
30
+ * `registrationOptions` / `createDecisionLog` 等函数与常量),**不是**某个模块的触发器声明。
31
+ * 声明由**消费方模块**导出,约定名为 `engineTriggers`(数组,元素交给 `validateTrigger`);
32
+ * 它与能力提供者的 `engineProvider` 登记**互斥**——同一模块不得同时导出两者,
33
+ * 该互斥由 `test/engine/provider-boundary.test.mjs` 直接断言。
34
+ * 因此「某个模块是触发器还是提供者」看的是它导出了哪一个,而不是它 import 了什么。
35
+ *
36
+ * ## 与 predicates 的接线要点(T1 交付时确认的事实)
37
+ *
38
+ * - `when` 直接接 `createXxxPredicate(...)` 的返回值——它们是**函数**,`await when(...)` 原生兼容;
39
+ * - **相位 / 计数**两类谓词带 `observe(session, event)` 入口:本文件在同组声明上**共用一条**
40
+ * `session/event` 监听喂它们(不按触发器各接一条),且没有这类谓词时**不接**(零开销);
41
+ * - **当前预设**类需在**挂载期一次** `await loadStandingMountFor()`,把结果传进
42
+ * `createPresetPredicate({ ctx, presetId, standingMountFor })`,这样判定期零 IO;
43
+ * - 谓词**运行期不抛**(配置错误在挂载期 fail loud),所以 `when` 的 try/catch 是兜底而非主路径。
44
+ *
45
+ * ## `state`:会话态声明的最小接口(B4 迁移的契约基准)
46
+ *
47
+ * `state` 是**声明**(该触发器需要什么会话态),不是运行时对象:运行时的会话态一律经
48
+ * `engine/shared.mjs` 的既有读法取得,**不新造接口、不新造存储通道**。B4 迁移既有模块时
49
+ * 只允许依赖下面这一组,超出即是新发明的接口,须先回到本文件扩契约:
50
+ *
51
+ * | 读取 | 既有实现 | 最小形状 |
52
+ * |--------------------|-----------------------------------|--------------------------------|
53
+ * | 会话标识(记账键) | `session.id` | `string \| undefined` |
54
+ * | durable 事件快照 | `shared.sessionEvents(session)` | `{ type, data? }[]`,缺接口=空 |
55
+ * | 轮号 | 各模块内联 `event.data.turn` | 有限 `number`,缺失=无轮号 |
56
+ * | 子代理判定 | `shared.isDelegated(session)` | `header.delegationDepth > 0` |
57
+ * | 会话态 Map 上限 | `shared.MAX_TRACKED_SESSIONS` | 超出整体清空(触发冷扫重建) |
58
+ *
59
+ * 三条纪律:**(1)** 没有 `session.id` 的会话一律不记账(`predicates.mjs:343` 的既有决定:
60
+ * 共用一个 Map 键会让两个会话串味),每次冷扫而非缓存;**(2)** 子代理判定复用
61
+ * `isDelegated`,不写字面量比较(原 `progress-reminder` 的 `== 0` 判定与它是同一语义的
62
+ * 两种写法,B4 迁移时统一到前者,行为不变);**(3)** `snapshotEvents()` 是正式 API
63
+ * (DSH 0.1.2-alpha.4+),缺失时按空日志处理——**不得**回退读旧的 `events` 数组。
64
+ *
65
+ * **给 B4 的边界(B4 PLAN 的措辞与本文件不一致处,以此为准)**:B4 计划里的
66
+ * `session(create, { resetOn })` **不能放进 `shared.mjs`**——`resetOn` 是复位时机,要落地
67
+ * 就得在挂载期注册 `session/event` 监听,而 `shared.mjs` 是**没有 `ctx` 的纯函数模块**
68
+ * (现有导出全是无状态工具)。B4 的统一访问接口须满足:**(a)** 接收挂载期 `ctx`(或由
69
+ * `mountTriggers` 一侧构造),**(b)** 建在本节这组既有读法之上、**不新造事件类型或持久通道**,
70
+ * **(c)** 逐模块保留原有键类型与淘汰策略(`Map<session.id>` / `WeakMap<session 对象>` /
71
+ * 无上限),**(d)** `resetOn` 按**状态字段**声明,不是按模块(`tool-bootstrap` 的
72
+ * `stage` 与 `promotion` 两个字段策略相反,是这条的现成反例)。
73
+ */
74
+
75
+ import { subjectOf } from './predicates.mjs'
76
+
77
+ /**
78
+ * 本文件此前**零 import**(`when` / `do` / `warnOnce` 全由调用方注入,会话态读法经 `ctx`
79
+ * 取得)。唯一新增的依赖是谓词的**输入契约** `subjectOf`:调度层调用 `when` 时必须先把
80
+ * 事件参数表归一为**单个载荷对象**(契约见 `predicates.mjs` 头部),否则除 `names` 外,
81
+ * 各原语在真实通道上都取不到它们要的域——`phase` 曾因此恒为「已晋升」、`count` 恒为 0。
82
+ * 声明路径同源:`actions.mjs` 的 `withWhen` 用同一函数归一。
83
+ */
84
+
85
+ /** 声明里的合法位置取值(与 events.ts 的布尔注册策略一一对应)。 */
86
+ export const WATERFALL_POSITIONS = new Set(['default', 'outermost'])
87
+
88
+ /** 动作在 `next()` 之前还是之后执行 —— 影响同通道内的可观测顺序。 */
89
+ export const ACTION_PHASES = new Set(['before-next', 'after-next'])
90
+
91
+ /**
92
+ * 同通道内的稳定排序:`channelOrder` 升序,同值保持**声明序**。
93
+ * 稳定是硬要求:同值声明的相对顺序若随排序实现变化,声明就不可预测。
94
+ */
95
+ export function orderTriggers(declarations) {
96
+ const list = Array.isArray(declarations) ? declarations : []
97
+ return list
98
+ .map((declaration, index) => ({ declaration, index }))
99
+ .sort((left, right) => {
100
+ const delta = (left.declaration.channelOrder ?? 0) - (right.declaration.channelOrder ?? 0)
101
+ return delta !== 0 ? delta : left.index - right.index
102
+ })
103
+ .map((entry) => entry.declaration)
104
+ }
105
+
106
+ /**
107
+ * 把声明的位置字段映射为 Cordis 的注册选项。
108
+ * 只有 `'outermost'` 才 prepend —— 这是**位置**,不是排序;同通道顺序归 `orderTriggers`。
109
+ */
110
+ export function registrationOptions(declaration) {
111
+ return declaration.waterfallPosition === 'outermost' ? { prepend: true } : {}
112
+ }
113
+
114
+ /**
115
+ * 校验一条触发器声明:
116
+ * - 必填:`id`(非空串)、`channel`(官方事件名)、`when`(判定函数)、`do`(动作函数);
117
+ * - `channelOrder` 缺省 0(非负安全整数)、`waterfallPosition` 缺省 `'default'`、
118
+ * `phase` 缺省 `'after-next'`、`degrade` 缺省告警并跳过本触发器。
119
+ * 未知字段一律 fail loud:声明写错键名是最容易被静默忽略的一类错。
120
+ */
121
+ export function validateTrigger(declaration, plugin) {
122
+ if (declaration === null || typeof declaration !== 'object' || Array.isArray(declaration)) {
123
+ throw new TypeError(`${plugin}: trigger declaration must be an object`)
124
+ }
125
+ const known = new Set(['id', 'channel', 'channelOrder', 'waterfallPosition', 'phase', 'when', 'do', 'degrade', 'state'])
126
+ const unknown = Object.keys(declaration).filter((key) => !known.has(key))
127
+ if (unknown.length > 0) {
128
+ throw new TypeError(`${plugin}: unknown trigger field(s) ${unknown.join(', ')} — allowed: ${[...known].sort().join(', ')}`)
129
+ }
130
+ if (typeof declaration.id !== 'string' || declaration.id.length === 0) {
131
+ throw new TypeError(`${plugin}: trigger.id must be a non-empty string`)
132
+ }
133
+ if (typeof declaration.channel !== 'string' || declaration.channel.length === 0) {
134
+ throw new TypeError(`${plugin}: trigger ${declaration.id}: channel must be a non-empty event name`)
135
+ }
136
+ if (typeof declaration.when !== 'function') {
137
+ throw new TypeError(`${plugin}: trigger ${declaration.id}: when must be a predicate function`)
138
+ }
139
+ if (typeof declaration.do !== 'function') {
140
+ throw new TypeError(`${plugin}: trigger ${declaration.id}: do must be an action function`)
141
+ }
142
+ const channelOrder = declaration.channelOrder ?? 0
143
+ if (!Number.isSafeInteger(channelOrder) || channelOrder < 0) {
144
+ throw new TypeError(`${plugin}: trigger ${declaration.id}: channelOrder must be a non-negative safe integer`)
145
+ }
146
+ const waterfallPosition = declaration.waterfallPosition ?? 'default'
147
+ if (!WATERFALL_POSITIONS.has(waterfallPosition)) {
148
+ throw new TypeError(`${plugin}: trigger ${declaration.id}: waterfallPosition must be one of ${[...WATERFALL_POSITIONS].join(', ')}`)
149
+ }
150
+ const phase = declaration.phase ?? 'after-next'
151
+ if (!ACTION_PHASES.has(phase)) {
152
+ throw new TypeError(`${plugin}: trigger ${declaration.id}: phase must be one of ${[...ACTION_PHASES].join(', ')}`)
153
+ }
154
+ return {
155
+ id: declaration.id,
156
+ channel: declaration.channel,
157
+ channelOrder,
158
+ waterfallPosition,
159
+ phase,
160
+ when: declaration.when,
161
+ do: declaration.do,
162
+ degrade: declaration.degrade ?? 'skip',
163
+ state: declaration.state,
164
+ }
165
+ }
166
+
167
+ /**
168
+ * 决策链诊断(T5):把「这次为什么没生效」变成一次可读的输出。
169
+ *
170
+ * 借鉴 dsh-agent-studio 的 `reportFirstDecision`(`apply.ts:238-250`):一次装配看起来
171
+ * 没生效时,原因可能有多种而**结果长得一样** —— 取不到 agent/session、触发器未启用、
172
+ * 谓词未命中、动作被外层门控剥离、判定走了降级。逐项试太慢,所以按顺序记一条链。
173
+ *
174
+ * 约束(PLAN T5 的 security):
175
+ * - **每会话只输出一次**(按 sessionId 去重);
176
+ * - 只记录触发器 id、通道、判定结果、动作是否执行、是否走降级 ——
177
+ * **绝不打印消息正文、变量值或用户内容**;
178
+ * - 默认关闭时零额外日志;日志自身抛错不得影响装配。
179
+ *
180
+ * **flush 由调用方驱动**:引擎无法知道「一轮装配」何时结束(同一通道可能有多个触发器、
181
+ * 跨通道还会继续判定),所以 `mountTriggers` 只 `step`(累积到内存),
182
+ * 由最外层监听器在整轮处理结束时调一次 `flush`。在步与步之间提前 flush 会输出**不完整的链**
183
+ * —— 那正是本文件最初实现踩过的坑(被 T5 自己的断言抓出来)。
184
+ */
185
+ export function createDecisionLog({ enabled = false, logger, sessionOf } = {}) {
186
+ const reported = new Set()
187
+ const pending = new Map()
188
+ // args 有两种形状:step 收到的是 handler 的完整参数数组,调用方 flush 时可能只给 payload。
189
+ // 两种都要归一到同一个 key —— 否则 step 存 's-1'、flush 查 '<no-session>',链永远输出不出来
190
+ //(这正是 T5 断言抓到的现象:logged.length 恒为 0)。
191
+ const keyOf = (args) => {
192
+ try {
193
+ const values = Array.isArray(args) ? args : [args]
194
+ return String(sessionOf?.(...values) ?? '<no-session>')
195
+ } catch {
196
+ return '<no-session>'
197
+ }
198
+ }
199
+ return {
200
+ /** 记一步(只入内存,不输出)。 */
201
+ step(args, line) {
202
+ if (!enabled) return
203
+ const key = keyOf(args)
204
+ const lines = pending.get(key) ?? []
205
+ lines.push(line)
206
+ pending.set(key, lines)
207
+ },
208
+ /** 该会话的链只输出一次;失败静默 —— 日志不得影响装配。 */
209
+ flush(args) {
210
+ if (!enabled) return
211
+ const key = keyOf(args)
212
+ if (reported.has(key)) return
213
+ const lines = pending.get(key)
214
+ if (lines === undefined || lines.length === 0) return
215
+ reported.add(key)
216
+ try {
217
+ logger?.info?.(`[trigger-decision] session=${key} :: ${lines.join(' | ')}`)
218
+ } catch {
219
+ // 日志失败不得影响装配。
220
+ }
221
+ },
222
+ }
223
+ }
224
+
225
+ /**
226
+ * 注册一组触发器。
227
+ *
228
+ * 每个触发器在**自己的合法通道**上注册一次;`when` 抛错按 `degrade` 处理
229
+ * (默认 `'skip'`:告警一次并跳过本次,**不吞掉下游异常** —— 只有本触发器自身的
230
+ * 判定失败才降级,`next()` 之后的错误照常向上抛)。
231
+ *
232
+ * @returns disposer:注销本组全部监听器。
233
+ */
234
+ /**
235
+ * 把 `session/event` 喂给带 `observe(session, event)` 入口的谓词(相位 / 计数两类)。
236
+ *
237
+ * 同一组声明**共用一条**监听(不按触发器各接一条,避免 N 倍监听);没有这类谓词时
238
+ * **不接**(返回 `undefined`,零开销)。观察者自身抛错只告警、不影响其它触发器
239
+ * ——与判定失败的降级纪律一致。
240
+ *
241
+ * 两条调用路径共用它:`mountTriggers`(`do` 是函数的内联路径)与
242
+ * `trigger-spec.mjs` 的 `mountDeclarations`(`do` 是动作声明的路径)。
243
+ *
244
+ * @returns disposer,或 `undefined`(本组无观察者)
245
+ */
246
+ export function wireTriggerObservers(ctx, triggers, { plugin, warnOnce } = {}) {
247
+ const warn = typeof warnOnce === 'function' ? warnOnce : () => {}
248
+ const observers = triggers.filter((trigger) => typeof trigger.when?.observe === 'function')
249
+ if (observers.length === 0) return undefined
250
+ return ctx.on('session/event', (session, event) => {
251
+ for (const trigger of observers) {
252
+ try {
253
+ trigger.when.observe(session, event)
254
+ } catch (error) {
255
+ warn(`${plugin}: trigger ${trigger.id} observe failed: ${String(error?.message ?? error)}`)
256
+ }
257
+ }
258
+ })
259
+ }
260
+
261
+ export function mountTriggers(ctx, declarations, { plugin, warnOnce, diagnose } = {}) {
262
+ const warn = typeof warnOnce === 'function' ? warnOnce : () => {}
263
+ const disposers = []
264
+ const ordered = orderTriggers((Array.isArray(declarations) ? declarations : []).map((raw) => validateTrigger(raw, plugin)))
265
+
266
+ // 会话态生命周期:共用 `wireTriggerObservers`(与声明路径同一实现)。
267
+ const observer = wireTriggerObservers(ctx, ordered, { plugin, warnOnce: warn })
268
+ if (observer !== undefined) disposers.push(observer)
269
+
270
+ for (const trigger of ordered) {
271
+ const handler = async (...args) => {
272
+ const next = args[args.length - 1]
273
+ const payload = args.slice(0, -1)
274
+ const result = trigger.phase === 'after-next' ? await next() : undefined
275
+ const pass = () => trigger.phase === 'after-next' ? result : next()
276
+ let decided
277
+ try {
278
+ decided = await trigger.when(subjectOf(trigger.channel, payload, warn))
279
+ } catch (error) {
280
+ // 判定失败只影响本触发器:告警一次并放行下游(expose-all 语义)。
281
+ warn(`${plugin}: trigger ${trigger.id} predicate failed: ${String(error?.message ?? error)}`)
282
+ diagnose?.step(args, `${trigger.id}@${trigger.channel} predicate=error action=skipped`)
283
+ return pass()
284
+ }
285
+ if (decided !== true) {
286
+ diagnose?.step(args, `${trigger.id}@${trigger.channel} predicate=miss action=skipped`)
287
+ return pass()
288
+ }
289
+ diagnose?.step(args, `${trigger.id}@${trigger.channel} predicate=hit phase=${trigger.phase} action=ran`)
290
+ if (trigger.phase === 'before-next') {
291
+ // 返回值参与瀑布:裁决类动作在 `next()` **之前**给出终局(deny/ask),
292
+ // 不返回值即「无话可说」,交给下游(与「只 guard 自身逻辑」一致)。
293
+ const early = await trigger.do(...payload)
294
+ return early === undefined ? next() : early
295
+ }
296
+ // 返回值同样参与瀑布:这正是裁决类动作(deny / ask / replace / block)的出口
297
+ // ——丢弃它会让「命中却拦不住」,是本文件被 trigger-rebuild 对拍抓到的缺陷。
298
+ const overridden = await trigger.do(...payload, result)
299
+ return overridden === undefined ? result : overridden
300
+ }
301
+ disposers.push(ctx.on(trigger.channel, handler, registrationOptions(trigger)))
302
+ }
303
+ return () => { for (const dispose of disposers) dispose() }
304
+ }
@@ -0,0 +1,11 @@
1
+ /** 世界书管理工具预设模块。 */
2
+ export const name = "world-book-tools"
3
+
4
+ export function apply(ctx) {
5
+ const service = ctx.get("pt-world-book-tools")
6
+ if (service === null || service === undefined || typeof service.mount !== "function") {
7
+ ctx.logger?.warn(name + ": service unavailable; world-book tools not mounted for this preset")
8
+ return
9
+ }
10
+ ctx.effect(() => service.mount(ctx), name + ": mount")
11
+ }