dsh-plugin-prompt-tool 0.8.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +84 -67
  2. package/engine/actions/append-context.mjs +49 -0
  3. package/engine/actions/assembly.mjs +150 -0
  4. package/engine/actions/catalog.mjs +92 -0
  5. package/engine/actions/content.mjs +205 -0
  6. package/engine/actions/decision.mjs +54 -0
  7. package/engine/actions/examples.mjs +11 -0
  8. package/engine/actions/guard.mjs +80 -0
  9. package/engine/actions/inbox-prepend.mjs +37 -0
  10. package/engine/actions/index.mjs +100 -0
  11. package/engine/actions/inject-text.mjs +22 -0
  12. package/engine/actions/pre-step-filter.mjs +55 -0
  13. package/engine/actions/registration.mjs +168 -0
  14. package/engine/actions/request-params.mjs +32 -0
  15. package/engine/actions/sdk-strip.mjs +30 -0
  16. package/engine/actions/shared.mjs +71 -0
  17. package/engine/actions.mjs +2 -931
  18. package/engine/branch.mjs +44 -0
  19. package/engine/character-tools.mjs +3 -6
  20. package/engine/compaction-epoch.mjs +31 -13
  21. package/engine/compositions/library/agent-instructions.yml +1 -1
  22. package/engine/compositions/library/command-goal.yml +1 -1
  23. package/engine/compositions/library/compaction.yml +1 -1
  24. package/engine/compositions/library/delegation-ptc.yml +1 -1
  25. package/engine/compositions/library/delegation.yml +1 -1
  26. package/engine/compositions/library/persistent-shell.yml +1 -1
  27. package/engine/compositions/library/planning.yml +1 -1
  28. package/engine/compositions/library/skill-filesystem-cordis.yml +1 -1
  29. package/engine/compositions/library/skill-filesystem.yml +1 -1
  30. package/engine/compositions/library/tool-ask-user.yml +1 -1
  31. package/engine/compositions/library/tool-bash.yml +1 -1
  32. package/engine/compositions/library/tool-cordis.yml +1 -1
  33. package/engine/compositions/library/tool-fs-search.yml +1 -1
  34. package/engine/compositions/library/tool-fs.yml +1 -1
  35. package/engine/compositions/library/tool-goal.yml +1 -1
  36. package/engine/compositions/library/tool-jobs.yml +1 -1
  37. package/engine/compositions/library/tool-plugin-manager-disabled.yml +1 -1
  38. package/engine/compositions/library/tool-plugin-manager.yml +1 -1
  39. package/engine/compositions/library/tool-present.yml +1 -1
  40. package/engine/compositions/library/tool-presentation.yml +1 -1
  41. package/engine/compositions/library/tool-pwsh.yml +1 -1
  42. package/engine/compositions/library/tool-skill.yml +1 -1
  43. package/engine/compositions/library/tool-todo.yml +1 -1
  44. package/engine/compositions/library/tool-web.yml +1 -1
  45. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  46. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  47. package/engine/compositions/source/local/rule-engine.yml +6 -0
  48. package/engine/condition.mjs +16 -3
  49. package/engine/conditions/anchor.mjs +32 -0
  50. package/engine/conditions/availability.mjs +47 -0
  51. package/engine/conditions/composite.mjs +83 -0
  52. package/engine/conditions/count.mjs +140 -0
  53. package/engine/conditions/evaluation.mjs +41 -0
  54. package/engine/conditions/examples.mjs +11 -0
  55. package/engine/conditions/index.mjs +85 -0
  56. package/engine/conditions/names.mjs +21 -0
  57. package/engine/conditions/phase.mjs +54 -0
  58. package/engine/conditions/preset.mjs +94 -0
  59. package/engine/conditions/scope.mjs +18 -0
  60. package/engine/conditions/session.mjs +22 -0
  61. package/engine/conditions/source.mjs +63 -0
  62. package/engine/conditions/subject.mjs +121 -0
  63. package/engine/conditions/text.mjs +44 -0
  64. package/engine/conditions/values.mjs +32 -0
  65. package/engine/dev-tool-search.mjs +213 -0
  66. package/engine/executor.mjs +93 -31
  67. package/engine/fillers.mjs +23 -16
  68. package/engine/instruction-hint.mjs +39 -23
  69. package/engine/layers.mjs +171 -77
  70. package/engine/order.mjs +20 -0
  71. package/engine/predicates.mjs +2 -753
  72. package/engine/rule-engine.mjs +40 -0
  73. package/engine/rule-runtime.mjs +110 -0
  74. package/engine/rule-spec.mjs +187 -0
  75. package/engine/schema.mjs +32 -11
  76. package/engine/session-var-tools.mjs +3 -6
  77. package/engine/shared.mjs +1 -1
  78. package/engine/st-render.mjs +8 -4
  79. package/engine/strategies.mjs +2 -235
  80. package/engine/subagent-tool-policy-core.mjs +7 -7
  81. package/engine/subagent-tool-policy.mjs +45 -20
  82. package/engine/tool-config-engine.mjs +53 -32
  83. package/engine/trigger-editor-meta.mjs +2 -21
  84. package/engine/trigger-spec.mjs +49 -133
  85. package/engine/world-book-tools.mjs +3 -6
  86. package/lib/client.js +10444 -10886
  87. package/lib/client.js.map +1 -1
  88. package/lib/index.d.mts +265 -361
  89. package/lib/index.mjs +1559 -2026
  90. package/lib/migrate-rules.d.mts +1 -0
  91. package/lib/migrate-rules.mjs +39 -0
  92. package/lib/module-package-CHPFweqK.mjs +2 -0
  93. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  94. package/lib/{paths-CY5psBqE.d.mts → paths-CyP0IkaM.d.mts} +388 -231
  95. package/lib/preset-transfer.d.mts +2 -2
  96. package/lib/preset-transfer.mjs +3 -2
  97. package/lib/{preset-package-BnJqrU4R.mjs → rules-migration-DJ6vASB5.mjs} +13825 -15283
  98. package/modules/ponytail/module.yml +406 -0
  99. package/modules/skill-surface/module.yml +74 -0
  100. package/modules/tool-surface/module.yml +67 -0
  101. package/package.json +50 -80
  102. package/templates/10-pre-step.yml +26 -22
  103. package/templates/14-first-turn-anchor.yml +25 -21
  104. package/templates/15-guide-auto.yml +34 -19
  105. package/templates/16-custom-fallback.yml +27 -15
  106. package/templates/18-placeholder-env-facts.yml +23 -14
  107. package/templates/20-system-section.yml +20 -11
  108. package/templates/30-runtime-context.yml +13 -7
  109. package/templates/40-agent-request.yml +11 -8
  110. package/templates/50-llm-stream.yml +14 -6
  111. package/templates/60-tool-pipeline.yml +15 -9
  112. package/templates/65-turn-stop.yml +19 -12
  113. package/templates/66-subagent-start.yml +12 -7
  114. package/templates/70-subagent-maintenance.yml +35 -29
  115. package/templates/75-subagent-end.yml +10 -5
  116. package/templates/80-tool-surface.yml +66 -0
  117. package/templates/policies/legacy-defaults.yml +34 -0
  118. package/templates/tools/50-delegate-worldbook.yml +1 -1
  119. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  120. package/engine/prompt-config-engine.mjs +0 -104
  121. package/preset/pt-cordis/preset.yml +0 -43
  122. package/preset/pt-cordis/skills/cordis-composition-reference/SKILL.md +0 -29
  123. package/preset/pt-cordis/skills/cordis-composition-reference/references/packages.md +0 -501
  124. package/preset/pt-cordis/skills/cordis-plugin-development/SKILL.md +0 -38
  125. package/preset/pt-cordis/skills/cordis-plugin-development/references/host-plugin.md +0 -60
  126. package/preset/pt-cordis/skills/cordis-plugin-development/references/mcp-bundle.md +0 -5
  127. package/preset/pt-cordis/skills/cordis-plugin-development/references/ui-plugin.md +0 -13
  128. package/preset/pt-cordis/skills/cordis-plugin-development/references/verification.md +0 -5
  129. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/client.js +0 -22
  130. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/cordis.patch.yml +0 -3
  131. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/index.js +0 -2
  132. package/preset/pt-cordis/skills/cordis-plugin-development/templates/decoration/package.json +0 -15
  133. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/cordis.patch.yml +0 -8
  134. package/preset/pt-cordis/skills/cordis-plugin-development/templates/mcp/package.json +0 -7
  135. package/preset/pt-cordis/skills/editing-cordis-compositions/SKILL.md +0 -84
  136. package/preset/pt-custom/preset.yml +0 -15
  137. package/preset/pt-minimal/preset.yml +0 -27
  138. package/preset/pt-ptc/preset.yml +0 -45
  139. package/preset/pt-standard/preset.yml +0 -43
@@ -0,0 +1,140 @@
1
+ import { MAX_TRACKED_SESSIONS, extractText, isDelegated, sessionEvents } from '../shared.mjs'
2
+ import { sessionOf } from './subject.mjs'
3
+ import { boundOf, optionalBoolean } from './values.mjs'
4
+
5
+ const COUNT_SIGNALS = {
6
+ 'tool-call': { type: 'tool/call', measure: 'count' },
7
+ 'tool-result': { type: 'tool/result', measure: 'count' },
8
+ 'assistant-message': { type: 'assistant/message', measure: 'count' },
9
+ 'assistant-chars': { type: 'assistant/message', measure: 'chars' },
10
+ 'user-message': { type: 'user/message', measure: 'count' },
11
+ turn: { type: 'turn/start', measure: 'count' },
12
+ }
13
+
14
+ /** 每会话最多保留的轮次计数(与 deliberation-gate 的 MAX_TRACKED_TURNS 同策略)。 */
15
+ const MAX_TRACKED_TURNS = 8
16
+
17
+ /** 轮边界事件:`per: 'turn'` 的当前轮由它与计数信号自身的轮号共同推进。 */
18
+ const TURN_EVENT = 'turn/start'
19
+
20
+ /**
21
+ * 计数判断:次数 / 字符数 / 轮次,带上下限与**冷启动重建**。
22
+ *
23
+ * - `per: 'session'` 计整个 durable log;`per: 'turn'` 只计**最新轮**,当前轮由
24
+ * `turn/start` 与计数信号自身带轮号的事件共同推进(与 deliberation-gate 的
25
+ * `turnOf` 同规则),轮号取自 `event.data.turn`;无轮号的事件不参与轮内计数
26
+ * (宁可少计,也不把跨轮数据算进当前轮)。
27
+ * - 冷启动:首次判定冷扫 `sessionEvents(session)` 重建(重启/恢复/重挂后同一结果),
28
+ * 之后 O(1);`observe` 是增量喂入,**只喂已判定过的会话**,避免「冷扫 + 增量」重复计数。
29
+ * - 上下限:`count >= min`(声明时)且 `count <= max`(声明时)。至少要声明一侧,
30
+ * 否则该判定恒真——那是把配置错误伪装成命中。
31
+ * - 取不到会话 = 无事件 = 计数 0(与空会话同一语义,不发明数据);无 `id` 的会话
32
+ * 不记账(共享一个 Map 键会让两个会话串味),每次冷扫。
33
+ *
34
+ * @param {object} options
35
+ * @param {'tool-call'|'tool-result'|'assistant-message'|'assistant-chars'|'user-message'|'turn'} options.of
36
+ * @param {'session'|'turn'} [options.per] 缺省 session
37
+ * @param {number} [options.min]
38
+ * @param {number} [options.max]
39
+ * @returns {((payload: unknown) => boolean) & { kind: string, observe: Function }}
40
+ */
41
+ export function createCountPredicate(options = {}) {
42
+ const signal = COUNT_SIGNALS[options.of]
43
+ if (signal === undefined) {
44
+ throw new TypeError(`predicates: count of must be one of ${Object.keys(COUNT_SIGNALS).join(', ')}`)
45
+ }
46
+ const per = options.per ?? 'session'
47
+ if (per !== 'session' && per !== 'turn') {
48
+ throw new TypeError('predicates: count per must be "session" or "turn"')
49
+ }
50
+ const min = boundOf(options.min, 'min')
51
+ const max = boundOf(options.max, 'max')
52
+ // 节奏判据(2026-09-22 用户拍板⑤「改用可重建计数」的核心语义):`every: N` = 每 N 次命中一次。
53
+ // 原 `progress-reminder` 的 `results % every === 0` 配的是**自增后**的计数
54
+ // (声明侧见 `test/engine/declarations/progress-reminder.yml` 的 `count.every`),
55
+ // 所以这里必须要求 `count > 0`——否则 `0 % N === 0` 会让第 0 次就命中。
56
+ const every = options.every === undefined
57
+ ? undefined
58
+ : Number.isSafeInteger(options.every) && options.every > 0
59
+ ? options.every
60
+ : (() => { throw new TypeError('predicates: count every must be a positive integer') })()
61
+ if (min === undefined && max === undefined && every === undefined) {
62
+ throw new TypeError('predicates: a count predicate needs min, max or every')
63
+ }
64
+ // 计数时点对齐(2026-09-22 用户拍板「扩原语」):某些通道的 `when` 在**处理器入口**求值,
65
+ // 早于本次信号事件落盘——`tools/post-execute` 就是 `dsh-tools` 的
66
+ // `finalizeScheduledExecution` 内部一环,而该包内不写 durable `tool/result`,落盘发生在
67
+ // `execute()` 返回后的 agent loop。`includeCurrent: true` 把「本次触发即将成为的那个事件」
68
+ // 计入,从而与模块自己的自增计数时点对齐。
69
+ // **适用边界由声明者负责**:谓词看不到通道名,只有在「当前事件即将成为该信号」的通道上
70
+ // 这个 +1 才成立(post-execute 上的 tool-result 成立;pre-execute 上的 tool-result 不成立)。
71
+ const includeCurrent = optionalBoolean(options.includeCurrent, 'includeCurrent', false)
72
+ // 受众判据(与 `session` 谓词的 `delegated` 同语义、同实现 `shared.isDelegated`)。
73
+ // 需求的来源是原 `deliberation-gate` 与原 `progress-reminder`:这两个能力的
74
+ // **主判据就是 count**,受众不能只挂在 `session` 谓词上——那会迫使声明额外耦合一个
75
+ // durable 事件类型(还要回答「哪种 type 恒真」,而哨兵轮表明甚至可能没有轮事件)。
76
+ const delegated = optionalBoolean(options.delegated, 'delegated', undefined)
77
+ /** sessionId -> { total, turns, lastTurn }(进程内快路径,真相在 durable 事件流)。 */
78
+ const state = new Map()
79
+ const fresh = () => ({ total: 0, turns: new Map(), lastTurn: undefined })
80
+ const measure = (event) => (signal.measure === 'chars' ? extractText(event.data).length : 1)
81
+ /** 轮号推进:与 deliberation-gate 的 turnOf/turnEntryOf 同规则——`turn/start` 与
82
+ * 计数信号自身带轮号的事件都推进当前轮,且取最大值;新轮先落 0,再累加。 */
83
+ const trackTurn = (entry, turn) => {
84
+ if (!entry.turns.has(turn)) {
85
+ if (entry.turns.size >= MAX_TRACKED_TURNS) {
86
+ const oldest = [...entry.turns.keys()]
87
+ .sort((left, right) => left - right)
88
+ .slice(0, entry.turns.size - MAX_TRACKED_TURNS + 1)
89
+ for (const key of oldest) entry.turns.delete(key)
90
+ }
91
+ entry.turns.set(turn, 0)
92
+ }
93
+ if (entry.lastTurn === undefined || turn > entry.lastTurn) entry.lastTurn = turn
94
+ }
95
+ const applyEvent = (entry, event) => {
96
+ const type = event?.type
97
+ const turn = event?.data?.turn
98
+ const tagged = typeof turn === 'number' && Number.isFinite(turn)
99
+ if (per === 'turn' && tagged && (type === TURN_EVENT || type === signal.type)) trackTurn(entry, turn)
100
+ if (type !== signal.type) return entry
101
+ const amount = measure(event)
102
+ if (per === 'session') {
103
+ entry.total += amount
104
+ return entry
105
+ }
106
+ // 轮内计数只认带轮号的事件(与 deliberation-gate 一致:宁可少计,也不把跨轮数据算进当前轮)。
107
+ if (!tagged) return entry
108
+ entry.turns.set(turn, (entry.turns.get(turn) ?? 0) + amount)
109
+ return entry
110
+ }
111
+ const countOf = (entry) => (per === 'session'
112
+ ? entry.total
113
+ : (entry.lastTurn === undefined ? 0 : entry.turns.get(entry.lastTurn) ?? 0))
114
+ const rebuild = (session) => {
115
+ const entry = fresh()
116
+ for (const event of sessionEvents(session)) applyEvent(entry, event)
117
+ return entry
118
+ }
119
+ const entryOf = (session) => {
120
+ if (session?.id === undefined) return rebuild(session)
121
+ const known = state.get(session.id)
122
+ if (known !== undefined) return known
123
+ const entry = rebuild(session)
124
+ if (state.size >= MAX_TRACKED_SESSIONS) state.clear()
125
+ state.set(session.id, entry)
126
+ return entry
127
+ }
128
+ const predicate = (payload) => {
129
+ if (delegated !== undefined && isDelegated(sessionOf(payload)) !== delegated) return false
130
+ const count = countOf(entryOf(sessionOf(payload))) + (includeCurrent ? 1 : 0)
131
+ if (every !== undefined && (count === 0 || count % every !== 0)) return false
132
+ return (min === undefined || count >= min) && (max === undefined || count <= max)
133
+ }
134
+ predicate.kind = 'count'
135
+ predicate.observe = (session, event) => {
136
+ const entry = session?.id === undefined ? undefined : state.get(session.id)
137
+ if (entry !== undefined) applyEvent(entry, event)
138
+ }
139
+ return predicate
140
+ }
@@ -0,0 +1,41 @@
1
+ import { subjectOf } from './subject.mjs'
2
+
3
+ /**
4
+ * 一次官方调用的局部判定帧;不按 session、turn 或载荷身份缓存。
5
+ * `ctx` 是判定期服务入口:子代理事件靠它用 `id` 反查 agent(见 subject.mjs 的
6
+ * `subagentFacts`),使 `modelScope`/`audience` 在判定阶段就有事实可用。
7
+ */
8
+ export function ruleFrame(channel, args, warnOnce = () => {}, ctx) {
9
+ return { channel, args, subject: subjectOf(channel, args, warnOnce, ctx), decisions: new Map(), warnOnce }
10
+ }
11
+
12
+ export function ruleMatches(rule, frame) {
13
+ if (rule === undefined) return true
14
+ if (frame.decisions.has(rule)) return frame.decisions.get(rule)
15
+ let hit = rule.enabled !== false
16
+ 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)}`) }
19
+ }
20
+ frame.decisions.set(rule, hit)
21
+ return hit
22
+ }
23
+
24
+ /**
25
+ * 动作级判定:规则条件与动作自身的分支条件都要过。
26
+ *
27
+ * `bypassRuleWhen` 的动作来自规则级 `else`——它的条件里已经含 `not(if)`,再叠加
28
+ * `rule.when` 会自相矛盾、令该分支永不执行。动作条件缺省 = 恒真;非 `true`
29
+ * (含 UNAVAILABLE 三值语义)一律不命中,与 `ruleMatches` 同一纪律。
30
+ */
31
+ 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
40
+ }
41
+ }
@@ -0,0 +1,11 @@
1
+ export const PREDICATE_EXAMPLES = {
2
+ text: { subject: 'userMessage', keys: ['keyword'] },
3
+ phase: { promoted: false },
4
+ source: { kind: 'user' },
5
+ count: { of: 'tool-call', per: 'turn', min: 1 },
6
+ names: { allow: ['bash'] },
7
+ session: { type: 'user/message', present: false },
8
+ preset: { presetId: 'preset-id' },
9
+ scope: { audience: 'main', modelScope: 'all' },
10
+ anchor: { keys: ['We'], fallbackAfter: 1 },
11
+ }
@@ -0,0 +1,85 @@
1
+ import { createTextPredicate } from './text.mjs'
2
+ import { createPhasePredicate } from './phase.mjs'
3
+ import { createSourcePredicate } from './source.mjs'
4
+ import { createCountPredicate } from './count.mjs'
5
+ import { createNameListPredicate } from './names.mjs'
6
+ import { createSessionStatePredicate } from './session.mjs'
7
+ import { createPresetPredicate } from './preset.mjs'
8
+ import { createScopePredicate } from './scope.mjs'
9
+ import { createAnchorPredicate } from './anchor.mjs'
10
+ import { withAvailableInput } from './availability.mjs'
11
+ import { composite } from './composite.mjs'
12
+ export * from './text.mjs'
13
+ export * from './phase.mjs'
14
+ export * from './source.mjs'
15
+ export * from './count.mjs'
16
+ export * from './names.mjs'
17
+ export * from './session.mjs'
18
+ export * from './preset.mjs'
19
+ export * from './scope.mjs'
20
+ export * from './anchor.mjs'
21
+ export { subjectOf } from './subject.mjs'
22
+ export { composite } from './composite.mjs'
23
+ export const PREDICATE_FACTORIES = Object.freeze({
24
+ text: createTextPredicate,
25
+ phase: createPhasePredicate,
26
+ source: createSourcePredicate,
27
+ count: createCountPredicate,
28
+ names: createNameListPredicate,
29
+ session: createSessionStatePredicate,
30
+ preset: createPresetPredicate,
31
+ scope: createScopePredicate,
32
+ anchor: createAnchorPredicate,
33
+ })
34
+ export const COMPOSITE_OPERATORS = Object.freeze(['any', 'all', 'not', 'notAny'])
35
+
36
+ const objectOrUndefined = (value, label) => {
37
+ if (value === undefined || value === null) return undefined
38
+ if (typeof value !== 'object' || Array.isArray(value)) {
39
+ throw new TypeError(`trigger-spec: ${label} must be an object`)
40
+ }
41
+ return value
42
+ }
43
+
44
+ export function compileWhen(node, context = {}) {
45
+ if (node === undefined || node === null) return undefined
46
+ 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)')
48
+ }
49
+ const keys = Object.keys(node)
50
+ if (keys.length === 0) {
51
+ throw new TypeError('trigger-spec: when must not be an empty object — 空节点是「恒真」还是「写漏了」无法区分')
52
+ }
53
+ if (keys.length > 1) {
54
+ throw new TypeError(
55
+ `trigger-spec: when must declare exactly one key — got ${keys.sort().join(', ')}`
56
+ + `(allowed: ${[...COMPOSITE_OPERATORS, ...Object.keys(PREDICATE_FACTORIES)].sort().join(', ')})`,
57
+ )
58
+ }
59
+ const [key] = keys
60
+ const value = node[key]
61
+
62
+ if (COMPOSITE_OPERATORS.includes(key)) {
63
+ if (key === 'not') {
64
+ return composite({ not: compileWhen(value, context) })
65
+ }
66
+ if (!Array.isArray(value) || value.length === 0) {
67
+ throw new TypeError(`trigger-spec: when.${key} must be a non-empty array of nodes`)
68
+ }
69
+ return composite({ [key]: value.map((child) => compileWhen(child, context)) })
70
+ }
71
+
72
+ const factory = PREDICATE_FACTORIES[key]
73
+ if (factory === undefined) {
74
+ throw new TypeError(
75
+ `trigger-spec: unknown predicate ${JSON.stringify(key)}`
76
+ + ` — known predicates: ${Object.keys(PREDICATE_FACTORIES).join(', ')}; composite: ${COMPOSITE_OPERATORS.join(', ')}`,
77
+ )
78
+ }
79
+ const options = objectOrUndefined(value, `when.${key}`) ?? {}
80
+ // `preset` 原语的取法需要挂载期注入(见 predicates.mjs 的三条硬约束),其余原语只用选项。
81
+ if (key === 'preset') {
82
+ return withAvailableInput(key, options, createPresetPredicate({ ...options, ctx: context.ctx, standingMountFor: context.standingMountFor }))
83
+ }
84
+ return withAvailableInput(key, options, factory(options))
85
+ }
@@ -0,0 +1,21 @@
1
+ import { nameList, optionalBoolean } from './values.mjs'
2
+ import { conditionInputAvailable, UNAVAILABLE } from './availability.mjs'
3
+
4
+ export function createNameListPredicate(options = {}) {
5
+ const allow = nameList(options.allow, 'allow')
6
+ const deny = nameList(options.deny, 'deny')
7
+ const caseSensitive = optionalBoolean(options.caseSensitive, 'caseSensitive', true)
8
+ const normalize = (name) => (caseSensitive ? name : name.toLowerCase())
9
+ const allowSet = allow === undefined ? undefined : new Set(allow.map(normalize))
10
+ const denySet = deny === undefined ? undefined : new Set(deny.map(normalize))
11
+ const predicate = (payload) => {
12
+ if (!conditionInputAvailable('names', options, payload)) return UNAVAILABLE
13
+ const name = typeof payload === 'string' ? payload : payload?.name
14
+ const value = normalize(name)
15
+ if (denySet !== undefined && denySet.has(value)) return false
16
+ if (allowSet !== undefined && !allowSet.has(value)) return false
17
+ return true
18
+ }
19
+ predicate.kind = 'names'
20
+ return predicate
21
+ }
@@ -0,0 +1,54 @@
1
+ import { createEpochPromotion } from '../compaction-epoch.mjs'
2
+ import { parsePromoteOn } from '../shared.mjs'
3
+ import { agentOf } from './subject.mjs'
4
+ import { optionalBoolean } from './values.mjs'
5
+
6
+ export function createPhasePredicate(options = {}) {
7
+ const promoteEvents = options.promoteEvents ?? parsePromoteOn('predicates', options.promoteOn)
8
+ if (!Array.isArray(promoteEvents) || promoteEvents.some((type) => typeof type !== 'string')) {
9
+ throw new TypeError('predicates: promoteEvents must be an array of event type strings')
10
+ }
11
+ const subscribe = optionalBoolean(options.subscribe, 'subscribe', true)
12
+ const trackerOptions = {
13
+ includeSubagents: options.includeSubagents === true,
14
+ promoteGate: options.promoteGate === true,
15
+ promoteAfterFirstResponse: options.promoteAfterFirstResponse === true,
16
+ maxPromoteSteps: options.maxPromoteSteps,
17
+ reasoningPattern: options.reasoningPattern,
18
+ reasoningNegativePattern: options.reasoningNegativePattern,
19
+ reasoningFlags: options.reasoningFlags,
20
+ }
21
+ const tracker = createEpochPromotion(promoteEvents, trackerOptions)
22
+ // 载荷归一后 agent 在 `payload.agent`;旧的手工形态(载荷本身就是 agent)由 agentOf 兼容。
23
+ //
24
+ // `compacted`(2026-09-22 用户拍板「扩原语」):`status()` 本来就返回 `boundary`
25
+ // (最近一次成功 `compaction/end` 的 seq,未压缩过为 -1),此前只用了 `promoted`。
26
+ // `compacted: true` = 发生过成功压缩;`false` = 从未压缩;未声明 = 不判该维度。
27
+ const compacted = optionalBoolean(options.compacted, 'compacted', undefined)
28
+ // `promoted` 三态:缺省 `true`(= 原行为,向后兼容)/ `false` / `'ignore'`。
29
+ //
30
+ // 为什么需要它(对拍实测):原实现把 `compacted` 与 `promoted` 写成**合取**,于是可表达的
31
+ // 原子只有 `P`、`C∧P`、`¬C∧P`——三者在 **¬P 的会话上全为 false**,用 `any`/`all`/`not`/
32
+ // `notAny` 的任何组合都表达不出原 `tool-bootstrap` compaction 回退要的 **`C ∧ ¬P`**
33
+ // (受控相位 + 已压缩:压缩后回到受控工具集;声明侧见
34
+ // `test/engine/declarations/tool-bootstrap.yml` 声明 2)。解耦后
35
+ // `{ compacted: true, promoted: false }` 就是 `C ∧ ¬P`。
36
+ const promotedOption = options.promoted
37
+ if (promotedOption !== undefined && promotedOption !== true && promotedOption !== false && promotedOption !== 'ignore') {
38
+ throw new TypeError("predicates: phase promoted must be true, false or 'ignore'")
39
+ }
40
+ if (promotedOption === 'ignore' && compacted === undefined) {
41
+ throw new TypeError("predicates: phase promoted 'ignore' needs compacted — 否则该判定恒真")
42
+ }
43
+ const predicate = (payload) => {
44
+ const status = (subscribe ? tracker : createEpochPromotion(promoteEvents, trackerOptions))
45
+ .status(agentOf(payload))
46
+ if (compacted !== undefined && (status.boundary >= 0) !== compacted) return false
47
+ if (promotedOption === 'ignore') return true
48
+ return status.promoted === (promotedOption ?? true)
49
+ }
50
+ predicate.kind = 'phase'
51
+ // 只有订阅模式才有增量入口:不订阅时多一个入口只会诱使调用方以为状态被记住了。
52
+ if (subscribe) predicate.observe = (session, event) => tracker.observe(session, event)
53
+ return predicate
54
+ }
@@ -0,0 +1,94 @@
1
+ import { importHostPackage } from '../host-package.mjs'
2
+ import { getService } from '../shared.mjs'
3
+ import { agentOf } from './subject.mjs'
4
+ import { UNAVAILABLE } from './availability.mjs'
5
+
6
+ const AGENT_PRESETS_PACKAGE = '@deepseek-ai/dsh-agent-preset-registry'
7
+
8
+ /**
9
+ * 当前预设 id(进程内判定;官方唯一正确取法)。
10
+ *
11
+ * const id = ctx.get?.('agentPresets')?.composedPreset?.(agent.ctx) // 首选
12
+ * ?? standingMountFor(agent.ctx)?.presetId // 服务未就绪时的同源兜底
13
+ *
14
+ * 依据:官方 0.1.7 的 `agent-preset-registry/src/index.ts` 中 composedPreset
15
+ * 直接包装 `standingMountFor(agentCtx)?.presetId`;mount.ts 读取 live scope chain,
16
+ * invariant.ts 也以该结果判定 agent 是否已绑定预设,不依赖事件落盘。
17
+ *
18
+ * 三条硬约束(改这里之前先读 PLAN 的「审查结论」):
19
+ * 1. **禁止读 `session.header.agentPreset`**——它是出生预设。官方 `agent-preset-registry/src/session.ts`
20
+ * 明写「Reconstruction reads the `agentPreset` Session projection, never the header alone」,
21
+ * 投影初值取自 header、之后由 `agent-preset/selected` 推进;真机实测亦有 header 与
22
+ * 实际挂载不一致的案例。
23
+ * 2. **禁止用 registry 的 defaultId** 当当前预设——它是
24
+ * **新会话的默认值**,与任何已存在 agent 的挂载无关。
25
+ * 3. **`undefined` 的含义是「该 agent 没有预设」**(裸 agent),不是「取不到」;
26
+ * **不得回退到默认值**,调用方按「不干预 / 不命中」处理。
27
+ *
28
+ * 适用边界:`composedPreset` 只在**进程内、拿得到 `agent.ctx`** 时可用(装配期
29
+ * `context.agent.ctx`、子代理认领、引擎能力)。跨 API/客户端边界没有这个方法——
30
+ * 官方 `ctx.remote.agentPresets` 提供 `list / select` 等远程操作,
31
+ * 客户端拿不到 `agent.ctx` 也无法用本方法。**两种取法是分工而非替代**:
32
+ * 进程内判定用本函数,跨边界用会话投影(`sessionProjections.stateOf(session, 'agentPreset')`)。
33
+ *
34
+ * @param {object|undefined} ctx 插件/运行时 ctx(服务查询基准)
35
+ * @param {{ctx?: unknown}|undefined} agent 判定对象(读它的 `ctx` = agent scope context)
36
+ * @param {Function} [standingMountFor] 官方模块导出兜底(服务未就绪时用)
37
+ * @returns {string|undefined} 预设 id;`undefined` = 该 agent 没有预设
38
+ */
39
+ export function agentPresetId(ctx, agent, standingMountFor) {
40
+ try {
41
+ const service = getService(ctx, 'agentPresets')
42
+ const composed = typeof service?.composedPreset === 'function'
43
+ ? service.composedPreset(agent?.ctx)
44
+ : undefined
45
+ if (composed !== undefined && composed !== null) return composed
46
+ const mount = typeof standingMountFor === 'function' ? standingMountFor(agent?.ctx) : undefined
47
+ return mount?.presetId
48
+ } catch {
49
+ // 判定不抛出:拿不到就是「没有预设」(不命中),与 condition.mjs 的降级纪律一致。
50
+ return undefined
51
+ }
52
+ }
53
+
54
+ /**
55
+ * 惰性解析官方 `standingMountFor`(挂载期一次,判定期零 IO)。
56
+ * 官方包缺席(最小组合、测试环境)=没有兜底,不是错误:此时服务缺席即判定恒 undefined。
57
+ * @param {string} [entry] 解析基准(缺省按宿主入口解析;测试/嵌入可显式指定)
58
+ * @returns {Promise<Function|undefined>}
59
+ */
60
+ export async function loadStandingMountFor(entry) {
61
+ try {
62
+ const module = await importHostPackage(AGENT_PRESETS_PACKAGE, entry)
63
+ return typeof module?.standingMountFor === 'function' ? module.standingMountFor : undefined
64
+ } catch {
65
+ return undefined
66
+ }
67
+ }
68
+
69
+ /**
70
+ * 当前预设判断:`agentPresetId(...) === presetId`。
71
+ *
72
+ * `ctx` 在**构造期**绑定(服务查询基准,通常是触发器运行时的插件 ctx);
73
+ * 判定只读 payload(agent)。无可确定的当前预设事实时保持 UNAVAILABLE,not 也不能放行。
74
+ *
75
+ * @param {object} options
76
+ * @param {string} options.presetId 目标预设 id
77
+ * @param {object} [options.ctx] 插件/运行时 ctx
78
+ * @param {Function} [options.standingMountFor] 官方模块导出兜底
79
+ * @returns {(agent: unknown) => boolean|symbol}
80
+ */
81
+ export function createPresetPredicate(options = {}) {
82
+ const presetId = options.presetId
83
+ if (typeof presetId !== 'string' || presetId.length === 0) {
84
+ throw new TypeError('predicates: a preset predicate needs a presetId')
85
+ }
86
+ const ctx = options.ctx
87
+ const standingMountFor = options.standingMountFor
88
+ const predicate = (payload) => {
89
+ const current = agentPresetId(ctx, agentOf(payload), standingMountFor)
90
+ return typeof current === 'string' && current.length > 0 ? current === presetId : UNAVAILABLE
91
+ }
92
+ predicate.kind = 'preset'
93
+ return predicate
94
+ }
@@ -0,0 +1,18 @@
1
+ import { isDelegated, matchesModel } from '../shared.mjs'
2
+ import { agentOf } from './subject.mjs'
3
+
4
+ export function createScopePredicate(options = {}) {
5
+ if (options.audience !== undefined && !['main', 'subagent'].includes(options.audience)) throw new TypeError('scope.audience must be main or subagent')
6
+ if (options.modelScope !== undefined && !['all', 'pro', 'flash'].includes(options.modelScope)) throw new TypeError('scope.modelScope must be all, pro or flash')
7
+ const predicate = payload => {
8
+ const agent = agentOf(payload)
9
+ const session = payload?.session ?? agent?.session
10
+ if (options.audience !== undefined) {
11
+ if (session === undefined) return false
12
+ if ((options.audience === 'subagent') !== isDelegated(session)) return false
13
+ }
14
+ return matchesModel(options.modelScope ?? 'all', payload?.model ?? agent?.options?.model)
15
+ }
16
+ predicate.kind = 'scope'
17
+ return predicate
18
+ }
@@ -0,0 +1,22 @@
1
+ import { isDelegated, sessionEvents } from '../shared.mjs'
2
+ import { sessionOf } from './subject.mjs'
3
+ import { optionalBoolean } from './values.mjs'
4
+
5
+ export function createSessionStatePredicate(options = {}) {
6
+ const type = options.type
7
+ if (typeof type !== 'string' || type.length === 0) {
8
+ throw new TypeError('predicates: a session-state predicate needs an event type')
9
+ }
10
+ const present = optionalBoolean(options.present, 'present', false)
11
+ // 受众判据(2026-09-22 用户拍板「扩原语」):`delegated: false` = 只在主会话命中,
12
+ // `true` = 只在子代理命中,未声明 = 不判受众。子代理判定复用 `shared.isDelegated`,
13
+ // 不写字面量比较(trigger.mjs 的会话态契约第 (2) 条纪律)。
14
+ const delegated = optionalBoolean(options.delegated, 'delegated', undefined)
15
+ const predicate = (payload) => {
16
+ if (delegated !== undefined && isDelegated(sessionOf(payload)) !== delegated) return false
17
+ return sessionEvents(sessionOf(payload))
18
+ .some((event) => event?.type === type) === present
19
+ }
20
+ predicate.kind = 'session'
21
+ return predicate
22
+ }
@@ -0,0 +1,63 @@
1
+ import { optionalBoolean } from './values.mjs'
2
+ import { conditionInputAvailable, UNAVAILABLE } from './availability.mjs'
3
+
4
+ function sourceChannel(value, field) {
5
+ if (value === undefined) return undefined
6
+ if (typeof value === 'string' && value.length > 0) return [value]
7
+ if (Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === 'string' && item.length > 0)) {
8
+ return [...new Set(value)]
9
+ }
10
+ throw new TypeError(`predicates: ${field} must be a non-empty string or an array of non-empty strings`)
11
+ }
12
+
13
+ /**
14
+ * 来源判断:`source.kind` / `source.plugin` 的比较(context-gate 的 allowKinds 一族
15
+ * 与 executor 的双通道去重都是这套比较)。
16
+ *
17
+ * - **精确**(缺省)为 `===` 语义,且**大小写敏感**——现有四处调用
18
+ * (context-gate / executor / anchor-turn / instruction-hint)全是精确且区分大小写,
19
+ * 统一不得放宽;大小写不敏感必须显式 `caseSensitive: false`。
20
+ * - **前缀**仅在 `match: 'prefix'` 时启用。
21
+ * - 声明了多个通道时是**合取**(都命中才算命中);需要析取用
22
+ * `composite({ any: [source({kind}), source({plugin})] })`——不隐式二选一。
23
+ *
24
+ * @param {object} options
25
+ * @param {string|string[]} [options.kind]
26
+ * @param {string|string[]} [options.plugin]
27
+ * @param {boolean} [options.caseSensitive] 缺省 true
28
+ * @param {'exact'|'prefix'} [options.match] 缺省 exact
29
+ * @returns {(payload: unknown) => boolean}
30
+ */
31
+ export function createSourcePredicate(options = {}) {
32
+ const kinds = sourceChannel(options.kind, 'kind')
33
+ const plugins = sourceChannel(options.plugin, 'plugin')
34
+ if (kinds === undefined && plugins === undefined) {
35
+ throw new TypeError('predicates: a source predicate needs kind or plugin')
36
+ }
37
+ const caseSensitive = optionalBoolean(options.caseSensitive, 'caseSensitive', true)
38
+ const match = options.match ?? 'exact'
39
+ if (match !== 'exact' && match !== 'prefix') {
40
+ throw new TypeError('predicates: source match must be "exact" or "prefix"')
41
+ }
42
+ const normalize = (value) => (caseSensitive ? value : value.toLowerCase())
43
+ const asSet = (list) => (list === undefined ? undefined : new Set(list.map(normalize)))
44
+ const kindSet = asSet(kinds)
45
+ const pluginSet = asSet(plugins)
46
+ const channelHit = (value, allowed) => {
47
+ if (allowed === undefined) return true
48
+ if (typeof value !== 'string' || value.length === 0) return false
49
+ const normalized = normalize(value)
50
+ if (match === 'exact') return allowed.has(normalized)
51
+ for (const prefix of allowed) {
52
+ if (normalized.startsWith(prefix)) return true
53
+ }
54
+ return false
55
+ }
56
+ const predicate = (payload) => {
57
+ if (!conditionInputAvailable('source', options, payload)) return UNAVAILABLE
58
+ const source = payload?.source
59
+ return channelHit(source?.kind, kindSet) && channelHit(source?.plugin, pluginSet)
60
+ }
61
+ predicate.kind = 'source'
62
+ return predicate
63
+ }