dsh-plugin-prompt-tool 1.0.0 → 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 (95) hide show
  1. package/README.md +38 -29
  2. package/engine/actions/append-context.mjs +49 -0
  3. package/engine/actions/assembly.mjs +150 -0
  4. package/engine/actions/catalog.mjs +92 -0
  5. package/engine/actions/content.mjs +205 -0
  6. package/engine/actions/decision.mjs +54 -0
  7. package/engine/actions/examples.mjs +11 -0
  8. package/engine/actions/guard.mjs +80 -0
  9. package/engine/actions/inbox-prepend.mjs +37 -0
  10. package/engine/actions/index.mjs +100 -0
  11. package/engine/actions/inject-text.mjs +22 -0
  12. package/engine/actions/pre-step-filter.mjs +55 -0
  13. package/engine/actions/registration.mjs +168 -0
  14. package/engine/actions/request-params.mjs +32 -0
  15. package/engine/actions/sdk-strip.mjs +30 -0
  16. package/engine/actions/shared.mjs +71 -0
  17. package/engine/actions.mjs +2 -931
  18. package/engine/branch.mjs +44 -0
  19. package/engine/compaction-epoch.mjs +31 -13
  20. package/engine/compositions/source/local/dev-tool-search.yml +11 -0
  21. package/engine/compositions/source/local/instruction-hint.yml +22 -1
  22. package/engine/compositions/source/local/rule-engine.yml +6 -0
  23. package/engine/condition.mjs +16 -3
  24. package/engine/conditions/anchor.mjs +32 -0
  25. package/engine/conditions/availability.mjs +47 -0
  26. package/engine/conditions/composite.mjs +83 -0
  27. package/engine/conditions/count.mjs +140 -0
  28. package/engine/conditions/evaluation.mjs +41 -0
  29. package/engine/conditions/examples.mjs +11 -0
  30. package/engine/conditions/index.mjs +85 -0
  31. package/engine/conditions/names.mjs +21 -0
  32. package/engine/conditions/phase.mjs +54 -0
  33. package/engine/conditions/preset.mjs +94 -0
  34. package/engine/conditions/scope.mjs +18 -0
  35. package/engine/conditions/session.mjs +22 -0
  36. package/engine/conditions/source.mjs +63 -0
  37. package/engine/conditions/subject.mjs +121 -0
  38. package/engine/conditions/text.mjs +44 -0
  39. package/engine/conditions/values.mjs +32 -0
  40. package/engine/dev-tool-search.mjs +213 -0
  41. package/engine/executor.mjs +55 -6
  42. package/engine/fillers.mjs +23 -16
  43. package/engine/instruction-hint.mjs +34 -22
  44. package/engine/layers.mjs +135 -49
  45. package/engine/order.mjs +4 -1
  46. package/engine/predicates.mjs +2 -753
  47. package/engine/rule-engine.mjs +40 -0
  48. package/engine/rule-runtime.mjs +110 -0
  49. package/engine/rule-spec.mjs +187 -0
  50. package/engine/schema.mjs +16 -5
  51. package/engine/shared.mjs +1 -1
  52. package/engine/strategies.mjs +2 -235
  53. package/engine/subagent-tool-policy-core.mjs +7 -7
  54. package/engine/subagent-tool-policy.mjs +20 -10
  55. package/engine/tool-config-engine.mjs +53 -32
  56. package/engine/trigger-editor-meta.mjs +2 -21
  57. package/engine/trigger-spec.mjs +49 -133
  58. package/lib/client.js +10446 -10820
  59. package/lib/client.js.map +1 -1
  60. package/lib/index.d.mts +82 -139
  61. package/lib/index.mjs +826 -1087
  62. package/lib/migrate-rules.d.mts +1 -0
  63. package/lib/migrate-rules.mjs +39 -0
  64. package/lib/module-package-CHPFweqK.mjs +2 -0
  65. package/lib/module-package-Cnd1_mwx.mjs +2872 -0
  66. package/lib/{paths-BgL42sSM.d.mts → paths-CyP0IkaM.d.mts} +223 -108
  67. package/lib/preset-transfer.d.mts +2 -2
  68. package/lib/preset-transfer.mjs +3 -2
  69. package/lib/{module-package-Cm3fHHlp.mjs → rules-migration-DJ6vASB5.mjs} +13696 -15557
  70. package/modules/ponytail/module.yml +381 -147
  71. package/modules/skill-surface/module.yml +74 -0
  72. package/modules/tool-surface/module.yml +67 -0
  73. package/package.json +5 -1
  74. package/templates/10-pre-step.yml +26 -22
  75. package/templates/14-first-turn-anchor.yml +25 -21
  76. package/templates/15-guide-auto.yml +34 -19
  77. package/templates/16-custom-fallback.yml +27 -15
  78. package/templates/18-placeholder-env-facts.yml +23 -14
  79. package/templates/20-system-section.yml +16 -10
  80. package/templates/30-runtime-context.yml +13 -7
  81. package/templates/40-agent-request.yml +11 -8
  82. package/templates/50-llm-stream.yml +14 -6
  83. package/templates/60-tool-pipeline.yml +15 -9
  84. package/templates/65-turn-stop.yml +19 -12
  85. package/templates/66-subagent-start.yml +12 -7
  86. package/templates/70-subagent-maintenance.yml +35 -29
  87. package/templates/75-subagent-end.yml +10 -5
  88. package/templates/80-tool-surface.yml +66 -0
  89. package/templates/policies/legacy-defaults.yml +34 -0
  90. package/engine/compositions/source/local/prompt-config-engine.yml +0 -7
  91. package/engine/prompt-config-engine.mjs +0 -104
  92. package/modules/ponytail/configs/0000-ponytail-rules.yml +0 -102
  93. package/modules/ponytail/configs/0010-ponytail-level-full.yml +0 -19
  94. package/modules/ponytail/configs/0020-ponytail-level-lite.yml +0 -19
  95. package/modules/ponytail/configs/0030-ponytail-level-ultra.yml +0 -19
@@ -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
+ }
@@ -0,0 +1,121 @@
1
+ import { lastAssistantText, subagentTextOf, toolArgsText, userMessagesText } from '../condition.mjs'
2
+ import { extractText } from '../shared.mjs'
3
+
4
+ const CHANNEL_SUBJECTS = {
5
+ 'system-prompt/assemble': (args) => ({ agent: args[1]?.agent }),
6
+ 'agent/request': (args) => ({ agent: args[0]?.agent }),
7
+ 'agent/pre-step': (args) => ({ agent: args[0]?.agent }),
8
+ 'agent/inbox/inserted': (args) => ({ agent: args[0]?.agent, source: args[0]?.message?.source }),
9
+ 'agent/turn-stopping': (args) => ({ agent: args[0]?.agent }),
10
+ 'tools/pre-execute': (args) => ({ agent: args[0]?.agent, name: args[0]?.name }),
11
+ 'tools/post-execute': (args) => ({ agent: args[0]?.agent, name: args[0]?.name }),
12
+ 'session/event': (args) => ({ session: args[0] }),
13
+ 'llm/stream': (args) => ({ model: args[0]?.model }),
14
+ 'subagent/start': () => ({}),
15
+ 'subagent/end': () => ({}),
16
+ }
17
+
18
+ /** 未知通道的告警去重(每个通道一次;判定期不刷屏)。 */
19
+ const warnedChannels = new Set()
20
+
21
+ /**
22
+ * 归一化载荷的标记(Symbol,非字符串键):`subjectOf` 产出它,`agentOf`/`sessionOf` 据此
23
+ * 区分「归一化载荷」与「旧形态的直接 payload」——**不用启发式猜**(曾按「有没有 `.session`」
24
+ * 判断,结果把测试里 `{ id, ctx }` 形状的假 agent 误判成归一化载荷)。
25
+ */
26
+ const SUBJECT_MARK = Symbol('predicate-subject')
27
+
28
+ /**
29
+ * 子代理事件判定期可见的 agent 事实。
30
+ *
31
+ * 官方 `subagent/start`、`subagent/end` 的载荷只有 `runId/provider/id/local`;
32
+ * `wireSubagentEvents` 是在 handler 里才把 child 挂上 frame 的,而 `ruleMatches`
33
+ * 在 handler **之前**执行。结果是 `scope.modelScope`(availability 要求 model 为非空
34
+ * 字符串)与 `scope.audience`(要求 session 可达)在子代理两层恒判 UNAVAILABLE——UI 上
35
+ * 配了却永远不命中,正是 schema 注释要避免的「配了没效果也不报错」。
36
+ *
37
+ * 这里在**判定前**用事件里的 `id` 反查 agent 补齐同样的事实。取不到时不写键:判定
38
+ * 退回 UNAVAILABLE(fail-closed),不得乐观放行。
39
+ */
40
+ function subagentFacts(info, ctx) {
41
+ const facts = {}
42
+ const id = info?.id
43
+ if (typeof id !== 'string' || id.length === 0) return facts
44
+ const agent = ctx?.agents?.get?.(id)
45
+ if (agent === undefined || agent === null) return facts
46
+ facts.agent = agent
47
+ if (agent.session !== undefined) facts.session = agent.session
48
+ const model = agent.options?.model
49
+ if (typeof model === 'string' && model.length > 0) facts.model = model
50
+ return facts
51
+ }
52
+
53
+ /**
54
+ * 把事件参数表归一为谓词载荷。
55
+ *
56
+ * `args` 传的是**去掉 `next` 之后的参数表**(无 `next` 的 emit/serial 通道原样传)。
57
+ * 未知通道回落到「第一个实参」(= 归一化之前的旧行为)并**告警一次**——不静默丢弃,
58
+ * 否则新增通道会悄悄失去判定能力;也不抛错,判定期抛错会打断会话。
59
+ *
60
+ * @param {string} channel 事件名
61
+ * @param {unknown[]} args 去掉 next 的参数表
62
+ * @param {Function} [warn] 告警回调(缺省静默)
63
+ * @param {unknown} [ctx] 判定期服务入口(目前只被子代理事件的 agent 反查使用)
64
+ * @returns {unknown} 谓词载荷
65
+ */
66
+ export function subjectOf(channel, args, warn, ctx) {
67
+ // 第一个实参的字段**原样保留**在载荷顶层:旧形态(如 `agent/turn-stopping` 的
68
+ // `{ agent, turn }`)因此继续可见,既有断言不会因归一化而失效。
69
+ const first = args[0]
70
+ const base = first !== null && typeof first === 'object' && !Array.isArray(first) ? { ...first } : {}
71
+ const pick = CHANNEL_SUBJECTS[channel]
72
+ const text = (() => {
73
+ switch (channel) {
74
+ case 'tools/pre-execute': return { argsText: toolArgsText(args[0]?.arguments) }
75
+ case 'tools/post-execute': return { argsText: toolArgsText(args[0]?.arguments), resultText: extractText(args[1]) }
76
+ case 'agent/pre-step': return { userText: userMessagesText(args[0]?.messages) }
77
+ case 'agent/inbox/inserted': return { userText: userMessagesText([args[0]?.message]) }
78
+ case 'agent/turn-stopping': return { assistantText: lastAssistantText(args[0]?.agent?.session) }
79
+ // 子代理事件的 `name` = provider(`SubagentRunInfo` 里唯一稳定的分类事实:
80
+ // spawn / fork / acp / codex / claude-code / dsh-sdk)。`names` 谓词读的就是
81
+ // `payload.name`,因此 `when: { names: { allow: ['fork'] } }` 在此可用。
82
+ // provider 可能缺席(官方注释:已接受的 one-shot 变 ready 或持久 Activation 冷恢复时
83
+ // 提供方未必仍注册)——那时不写键,让 `names` 走 UNAVAILABLE,而不是塞空串当「已知为空」。
84
+ case 'subagent/start':
85
+ case 'subagent/end': return {
86
+ subagentText: subagentTextOf(args[0]),
87
+ ...(typeof args[0]?.provider === 'string' && args[0].provider.length > 0 ? { name: args[0].provider } : {}),
88
+ ...subagentFacts(args[0], ctx),
89
+ }
90
+ default: return {}
91
+ }
92
+ })()
93
+ if (pick === undefined) {
94
+ if (typeof warn === 'function' && !warnedChannels.has(channel)) {
95
+ warnedChannels.add(channel)
96
+ warn(`predicates: unknown channel ${JSON.stringify(channel)} — 无显式取法,退回「第一个实参即载荷」的旧语义`)
97
+ }
98
+ return { ...base, ...text, [SUBJECT_MARK]: true, channel, args }
99
+ }
100
+ return { ...base, ...pick(args), ...text, [SUBJECT_MARK]: true, channel, args }
101
+ }
102
+
103
+ /**
104
+ * 从载荷取 agent:**归一化载荷**读 `agent`(`subjectOf` 的标记为凭);**旧形态**
105
+ * (载荷本身就是 agent——手写调用方与既有测试都这样传)原样返回。
106
+ */
107
+ export function agentOf(payload) {
108
+ if (payload === null || typeof payload !== 'object') return undefined
109
+ return payload[SUBJECT_MARK] === true ? payload.agent : payload
110
+ }
111
+
112
+ /**
113
+ * 从载荷取 session:归一化载荷走 `session` → `agent.session`;旧形态走
114
+ * `session` → 载荷自身。取不到时返回原载荷——`sessionEvents` 对无 `snapshotEvents`
115
+ * 的对象返回空,语义是「无事件」,与归一化之前逐例一致。
116
+ */
117
+ export function sessionOf(payload) {
118
+ if (payload === null || typeof payload !== 'object') return payload
119
+ if (payload[SUBJECT_MARK] !== true) return payload.session ?? payload
120
+ return payload.session ?? payload.agent?.session
121
+ }
@@ -0,0 +1,44 @@
1
+ import { MATCH_LOGIC, createAnchorMatcher } from '../anchor-match.mjs'
2
+ import { subjectTextOf } from '../condition.mjs'
3
+ import { keyList } from './values.mjs'
4
+ import { conditionInputAvailable, UNAVAILABLE } from './availability.mjs'
5
+ const MATCH_FLAGS = ['caseSensitive', 'wholeWords', 'useRegex', 'stWords']
6
+ const MATCH_MODES = ['scan', 'prefix']
7
+
8
+ export function createTextPredicate(options = {}) {
9
+ const keys = keyList(options.keys, 'keys')
10
+ const secondaryKeys = keyList(options.secondaryKeys, 'secondaryKeys')
11
+ if (keys.length === 0 && secondaryKeys.length === 0) {
12
+ throw new TypeError('predicates: a text predicate needs at least one non-empty key')
13
+ }
14
+ const logic = options.logic ?? MATCH_LOGIC.ANY
15
+ if (!Object.values(MATCH_LOGIC).includes(logic)) {
16
+ throw new TypeError(`predicates: logic must be one of ${Object.values(MATCH_LOGIC).join(', ')}`)
17
+ }
18
+ const mode = options.mode ?? 'scan'
19
+ if (!MATCH_MODES.includes(mode)) {
20
+ throw new TypeError(`predicates: mode must be one of ${MATCH_MODES.join(', ')}`)
21
+ }
22
+ for (const field of MATCH_FLAGS) {
23
+ if (options[field] !== undefined && typeof options[field] !== 'boolean') {
24
+ throw new TypeError(`predicates: ${field} must be a boolean when present`)
25
+ }
26
+ }
27
+ // 与 anchor-match 逐例等价的关键:匹配器就是 anchor-match 的匹配器,本层不加也不减语义。
28
+ const matcher = createAnchorMatcher({
29
+ keys,
30
+ secondaryKeys,
31
+ caseSensitive: options.caseSensitive === true,
32
+ wholeWords: options.wholeWords === true,
33
+ useRegex: options.useRegex,
34
+ logic,
35
+ mode,
36
+ stWords: options.stWords === true,
37
+ })
38
+ const subject = options.subject
39
+ const predicate = (payload) => conditionInputAvailable('text', options, payload)
40
+ ? matcher.scan(typeof payload === 'string' ? payload : subjectTextOf(subject, payload)).active === true
41
+ : UNAVAILABLE
42
+ predicate.kind = 'text'
43
+ return predicate
44
+ }
@@ -0,0 +1,32 @@
1
+ export function optionalBoolean(value, field, fallback) {
2
+ if (value === undefined) return fallback
3
+ if (typeof value !== 'boolean') throw new TypeError(`predicates: ${field} must be a boolean`)
4
+ return value
5
+ }
6
+
7
+ /** 键集合归一化:非字符串数组 / 非数组一律 fail loud;空白键丢弃。 */
8
+ export function keyList(value, field) {
9
+ if (value === undefined) return []
10
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
11
+ throw new TypeError(`predicates: ${field} must be an array of strings when present`)
12
+ }
13
+ return value.map((item) => item.trim()).filter((item) => item.length > 0)
14
+ }
15
+
16
+ /** 名称集合:与 tool-filter 的 nameSet 同严格度(空串不是合法名称)。 */
17
+ export function nameList(value, field) {
18
+ if (value === undefined) return undefined
19
+ if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
20
+ throw new TypeError(`predicates: ${field} must be an array of non-empty strings`)
21
+ }
22
+ return [...new Set(value)]
23
+ }
24
+
25
+ /** 计数上下限:缺省 = 不设该侧;非安全非负整数一律 fail loud。 */
26
+ export function boundOf(value, field) {
27
+ if (value === undefined) return undefined
28
+ if (!Number.isSafeInteger(value) || value < 0) {
29
+ throw new TypeError(`predicates: count ${field} must be an integer >= 0`)
30
+ }
31
+ return value
32
+ }