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,92 @@
1
+ export const ACTION_DEGRADE = Object.freeze({
2
+ keep: 'keep',
3
+ exposeAll: 'expose-all',
4
+ empty: 'empty',
5
+ silent: 'silent',
6
+ })
7
+
8
+ /**
9
+ * 七类动作的声明:**合法事件通道** + **降级语义** + 触发时机。
10
+ * `events` 是动作允许注册的官方事件(注册到声明外的事件在挂载期 fail loud);
11
+ * `services` 是它允许触碰的宿主服务方法(同一纪律的文档面)。
12
+ */
13
+ export const ACTION_KINDS = Object.freeze({
14
+ 'inject-text': {
15
+ title: '注入文本',
16
+ events: ['agent/pre-step', 'session/event', 'system-prompt/assemble', 'agent/request', 'llm/stream', 'tools/pre-execute', 'tools/post-execute', 'agent/turn-stopping', 'subagent/start', 'subagent/end'],
17
+ services: ['systemPrompt.section', 'systemPrompt.context', 'systemPrompt.variable'],
18
+ degrade: ACTION_DEGRADE.keep,
19
+ timing: '按配置声明的 layer 落到该层官方通道;pre-step 在 agent/pre-step,system-section/runtime-context 在注册期 + 官方 assembly。',
20
+ note: '复用九层注册通道,不新开通道;显式空正文 = 不注册(empty 情形)。`options` 原样交给该层的既有接线(pre-step 执行器的 `prepend` = 声明的位置策略);`session/event` 是 pre-step 执行器自带的会话态旁听,不是第二条注入通道。',
21
+ },
22
+ assembly: {
23
+ title: '改装配',
24
+ events: ['system-prompt/assemble'],
25
+ services: [],
26
+ degrade: ACTION_DEGRADE.exposeAll,
27
+ timing: 'system-prompt/assemble 的下游结算之后(`await next()` 之后改,装配事实已就绪)。',
28
+ note: '失败时返回未改动的装配 = 对 tools 即暴露完整目录;不改 global,只作用于本次 assembly。',
29
+ },
30
+ decision: {
31
+ title: '裁决',
32
+ events: ['tools/pre-execute', 'tools/post-execute'],
33
+ services: [],
34
+ degrade: ACTION_DEGRADE.keep,
35
+ timing: 'pre 相在工具执行之前返回 allow|deny|ask;post 相在结果结算之后返回 accept|replace|block。',
36
+ note: '异常与条件未命中一律 `next()`(放行/接受),绝不让裁决 bug 卡死调用。',
37
+ },
38
+ 'append-context': {
39
+ title: '追加上下文与续跑',
40
+ events: ['tools/post-execute', 'agent/turn-stopping'],
41
+ services: [],
42
+ degrade: ACTION_DEGRADE.keep,
43
+ timing: 'mode=context 在 post-execute 追加 durable user 通知;mode=continue 在 agent/turn-stopping steer 一步。',
44
+ note: '续跑共用引擎的续跑预算(每轮 1 次 / 每会话 3 次),无正文时不追加(empty 情形);绝不伪造 assistant 角色。',
45
+ },
46
+ guard: {
47
+ title: '执行层 guard',
48
+ events: ['system-prompt/assemble'],
49
+ services: ['tools.guard', 'tools.restrict'],
50
+ degrade: ACTION_DEGRADE.silent,
51
+ timing: '首次 assembly 时按 agent scope 惰性注册一次(预设切换/重绑后按新的 agent.ctx 重注册)。',
52
+ note: '最终拒绝层:注册在 agent.ctx 上(工具本层与晚到工具也被裁决),命中返回拒绝原因;不注册全局 guard。guard 自身不吞异常——异常即失败,绝不退化成"放行"。',
53
+ },
54
+ 'sdk-strip': {
55
+ title: '裁 SDK 声明文本',
56
+ events: ['system-prompt/assemble'],
57
+ services: [],
58
+ degrade: ACTION_DEGRADE.keep,
59
+ timing: 'system-prompt/assemble 下游结算之后,改写名为 tools:sdk 的段文本。',
60
+ note: '只删不增、保守失败:形态异常/自校验失败一律原样返回;空名单零开销。仅是呈现补救,执行边界在 guard。',
61
+ },
62
+ 'request-params': {
63
+ title: '改模型请求参数',
64
+ events: ['agent/request'],
65
+ services: [],
66
+ degrade: ACTION_DEGRADE.keep,
67
+ timing: 'agent/request 的下游结算之后(`await next()` 拿到已冻结的 LlmCallConfig 再改写)。',
68
+ note: '与既有 promptConfigs 的 agent-request 层共用 applyAgentRequestParams;只改命中 scope/agent 的请求,删键仅删"本声明注入的那个值"。',
69
+ },
70
+ 'inbox-prepend': {
71
+ title: '前置收件箱消息',
72
+ events: ['agent/inbox/inserted'],
73
+ services: ['agent.inbox.prepend'],
74
+ degrade: ACTION_DEGRADE.silent,
75
+ timing: 'emit 通道(**无 next**):命中即前置一条消息,不命中什么都不做。',
76
+ note: '只前置不追加;插件来源消息(含本动作自己插入的那条)永不再次前置,防自触发插队。'
77
+ + '收件箱的持久化归宿主(`agent/inbox/spliced`),本动作不自己落盘。',
78
+ },
79
+ 'pre-step-filter': {
80
+ title: '过滤 pre-step 注入消息',
81
+ events: ['agent/pre-step'],
82
+ services: [],
83
+ degrade: ACTION_DEGRADE.exposeAll,
84
+ timing: 'agent/pre-step 的下游结算之后(`await next()` 拿到 decision 再过滤 messages)。',
85
+ note: '两种白名单模式**互斥**:`sources` = 严格白名单(只放行这些 `source.kind`,含 claimed 批);'
86
+ + '`keepKinds` = 保留 claimed 基线(按对象身份或 id)+ 这些 kind。未声明任一 = 不过滤(零开销)。'
87
+ + '`blockPlugins` 是与二者**正交**的另一维度、可共存:仅当来源 kind 为 plugin 时按 `source.plugin` '
88
+ + '做大小写不敏感的**精确等值**匹配并剔除命中项,不做子串/正则/glob;空名单 = 关闭'
89
+ + '(与白名单「空 = 全拦」不同),未声明 = 不过滤。'
90
+ + '**永不吞上下文**:异常一律返回未过滤的 decision(`expose-all`)。',
91
+ },
92
+ })
@@ -0,0 +1,205 @@
1
+ /**
2
+ * strategies — 引擎内容策略绑定(config.resolve)。
3
+ * 内容策略: static / placeholder / first-turn-anchor / guide-auto / anchor-notice / world-book。
4
+ * 策略参数全部来自 config.params(由 preset.yml 单一配置源下发),引擎只负责组装。
5
+ * 仍支持 strategyDir 懒加载自定义模板策略。
6
+ */
7
+
8
+ import { extractText } from '../shared.mjs'
9
+ import { MATCH_LOGIC, createAnchorMatcher } from '../anchor-match.mjs'
10
+ import { createTaskClassifier } from '../classify-task.mjs'
11
+ import { createPlaceholderResolver } from '../fillers.mjs'
12
+ import { createAnchorState } from '../conditions/anchor.mjs'
13
+
14
+ const name = 'prompt-config-engine'
15
+
16
+ /**
17
+ * first-turn-anchor:首条真实用户消息后的一次性任务锚点。
18
+ * 正则与锚句文本全部来自 config.params(由 preset.yml 单一配置源下发)。
19
+ */
20
+ function createFirstTurnAnchorResolver(config) {
21
+ const useCustom = config.params?.useCustom === true
22
+ const customText = typeof config.params?.text === 'string' ? config.params.text : ''
23
+ const buildPattern = typeof config.params?.buildPattern === 'string' ? config.params.buildPattern : ''
24
+ const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
25
+ const firstTurnBuild = typeof config.params?.firstTurnBuild === 'string' ? config.params.firstTurnBuild : ''
26
+ const firstTurnInspect = typeof config.params?.firstTurnInspect === 'string' ? config.params.firstTurnInspect : ''
27
+ const firstTurnDeep = typeof config.params?.firstTurnDeep === 'string' ? config.params.firstTurnDeep : ''
28
+ const classifier = createTaskClassifier({ buildPattern, complexPattern })
29
+ return ({ messages }) => {
30
+ if (useCustom) {
31
+ const text = customText.trim()
32
+ return text.length > 0 ? { text } : null
33
+ }
34
+ if (!classifier.ready || (firstTurnBuild.length === 0 && firstTurnInspect.length === 0 && firstTurnDeep.length === 0)) {
35
+ return null
36
+ }
37
+ const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
38
+ if (userIndex < 0) return null
39
+ const taskText = extractText(messages[userIndex])
40
+ if (taskText.length === 0) return null
41
+ const kind = classifier.classify(taskText)
42
+ const anchor = kind === 'complex' ? firstTurnDeep : kind === 'build' ? firstTurnBuild : firstTurnInspect
43
+ return anchor.length > 0 ? { text: anchor } : null
44
+ }
45
+ }
46
+
47
+ /**
48
+ * guide-auto:晋升后每轮用户消息后的弱/深度引导。
49
+ * 正则与引导文本全部来自 config.params(由 preset.yml 单一配置源下发)。
50
+ * 复杂任务判定 fallback 复用锚定功能的 complexPattern(guideComplexPattern
51
+ * 冗余副本已移除)——锚定与引导是独立功能,仅分类器共用。
52
+ */
53
+ function createGuideAutoResolver(config) {
54
+ const useCustom = config.params?.useCustom === true
55
+ const customText = typeof config.params?.text === 'string' ? config.params.text : ''
56
+ const complexPattern = typeof config.params?.complexPattern === 'string' ? config.params.complexPattern : ''
57
+ const guideWeak = typeof config.params?.guideWeak === 'string' ? config.params.guideWeak : ''
58
+ const guideDeep = typeof config.params?.guideDeep === 'string' ? config.params.guideDeep : ''
59
+ const rawMinChars = config.params?.complexMinChars
60
+ const complexMinChars = rawMinChars == null || rawMinChars === '' ? undefined : rawMinChars
61
+ if (complexMinChars !== undefined && (!Number.isSafeInteger(complexMinChars) || complexMinChars < 0)) throw new TypeError(`${name}: guide-auto complexMinChars must be a non-negative integer`)
62
+ // 复杂判定 fallback 复用锚定分类器(buildPattern 不参与引导判定)。
63
+ const classifier = createTaskClassifier({ complexPattern })
64
+ return ({ messages }) => {
65
+ const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
66
+ if (userIndex < 0) return null
67
+ const text = extractText(messages[userIndex])
68
+ if (text.length === 0) return null
69
+ if (useCustom) {
70
+ const guide = customText.trim()
71
+ return guide.length > 0 ? { text: guide } : null
72
+ }
73
+ if (guideWeak.length === 0 && guideDeep.length === 0) return null
74
+ return { text: ((complexMinChars !== undefined && text.length > complexMinChars) || classifier.isComplex(text)) ? guideDeep : guideWeak }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * anchor-notice 只生成显式正文与来源说明;资格由规则条件单独负责。
80
+ */
81
+ function createAnchorNoticeResolver(config) {
82
+ const promptText = config.texts.length > 0
83
+ ? config.texts.join('\n\n')
84
+ : (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
85
+ // 确认词无内置默认(默认值归模板/预设):空 = 不做词确认,只走未命中兜底注入。
86
+ const firstTurnWord = typeof config.params?.firstTurnWord === 'string' ? config.params.firstTurnWord : ''
87
+ // 确认词集合:writePreset 派生的 anchorWords 优先(锚句信号词多词 prefix,任一命中即
88
+ // 确认——deep 档 Let…/自定义锚句首词都覆盖);无 anchorWords 时用显式确认词。
89
+ const anchorWords = Array.isArray(config.params?.anchorWords) && config.params.anchorWords.length > 0
90
+ ? config.params.anchorWords.map(String).filter((word) => word.length > 0)
91
+ : (firstTurnWord.length > 0 ? [firstTurnWord] : [])
92
+ const inspect = createAnchorState({ keys: anchorWords })
93
+
94
+ return ({ agent }) => {
95
+ if (promptText === undefined) return null
96
+ const { confirmed } = inspect(agent.session)
97
+ return {
98
+ text: promptText,
99
+ source: {
100
+ // v4 要求 kind 是生产者名:裸 `plugin` 是退役包装,会被会话格式校验拒绝。
101
+ kind: `plugin:${config.id}`,
102
+ plugin: config.id,
103
+ form: 'notice',
104
+ summary: confirmed
105
+ ? (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」锚定确认后注入)` : 'prompt-tool 提示词(无确认词,锚定就绪后注入)')
106
+ : (firstTurnWord.length > 0 ? `prompt-tool 提示词(「${firstTurnWord}」未确认,兜底注入)` : 'prompt-tool 提示词(无确认词,兜底注入)'),
107
+ },
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * world-book:世界书条目(角色卡 lorebook)。constant=true 恒注入(不扫 keys);
114
+ * selective 条目扫描当前消息批文本,keys/secondaryKeys 按 selectiveLogic 组合
115
+ * (any=任一命中 / all=副键全中 / not=副键全不中)。匹配经 anchor-match 引擎。
116
+ */
117
+ function createWorldBookResolver(config) {
118
+ if (config.params?.stWorldBook) return ({ stWorldBookSelected }) => stWorldBookSelected === true ? { text: config.texts.join('\n\n') } : null
119
+ const constant = config.params?.constant === true
120
+ // selectiveLogic:ST world_info_logic 0=AND_ANY 1=NOT_ALL 2=NOT_ANY 3=AND_ALL。
121
+ const rawLogic = config.params?.selectiveLogic
122
+ const logic = rawLogic === 3
123
+ ? MATCH_LOGIC.ALL
124
+ : (rawLogic === 1
125
+ ? MATCH_LOGIC.NOT
126
+ : (rawLogic === 2 ? MATCH_LOGIC.NOT_ANY : MATCH_LOGIC.ANY))
127
+ const matcher = createAnchorMatcher({
128
+ keys: Array.isArray(config.params?.keys) ? config.params.keys : [],
129
+ secondaryKeys: Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : [],
130
+ caseSensitive: config.params?.caseSensitive === true,
131
+ wholeWords: config.params?.wholeWords === true,
132
+ // 三态透传:undefined=ST 语义自动检测(/regex/ 或含特殊字符键按正则);
133
+ // 存量配置显式 true/false 仍强制对应行为。
134
+ useRegex: config.params?.useRegex,
135
+ logic,
136
+ })
137
+ // ST 语义:constant 或无任何键的条目恒注入(always active)。
138
+ const hasAnyKey = (Array.isArray(config.params?.keys) ? config.params.keys : [])
139
+ .some((key) => String(key).trim().length > 0)
140
+ || (Array.isArray(config.params?.secondaryKeys) ? config.params.secondaryKeys : [])
141
+ .some((key) => String(key).trim().length > 0)
142
+ const promptText = config.texts.length > 0
143
+ ? config.texts.join('\n\n')
144
+ : (typeof config.params?.text === 'string' && config.params.text.length > 0 ? config.params.text : undefined)
145
+ return ({ messages }) => {
146
+ if (promptText === undefined) return null
147
+ if (constant || !hasAnyKey) return { text: promptText }
148
+ const haystack = (Array.isArray(messages) ? messages : [])
149
+ .map((message) => extractText(message))
150
+ .filter((text) => text.length > 0)
151
+ .join('\n')
152
+ if (haystack.length === 0) return null
153
+ return matcher.scan(haystack).active ? { text: promptText } : null
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 为归一化后的提示词配置绑定策略 resolve(策略状态随提示词配置对象,不随 apply)。
159
+ * strategyDir:外部策略模块目录(相对本文件 URL)。未声明时只允许内置策略。
160
+ */
161
+ export function bindResolver(config, strategyDir) {
162
+ switch (config.strategy) {
163
+ case 'placeholder': return createPlaceholderResolver(config)
164
+ case 'first-turn-anchor': return createFirstTurnAnchorResolver(config)
165
+ case 'guide-auto': return createGuideAutoResolver(config)
166
+ case 'custom-fallback':
167
+ throw new TypeError(`${name}: migrate custom-fallback to an anchor condition and anchor-notice content`)
168
+ case 'anchor-notice':
169
+ return createAnchorNoticeResolver(config)
170
+ case 'world-book':
171
+ return createWorldBookResolver(config)
172
+ case 'static': {
173
+ const texts = config.texts
174
+ const patch = config.templatePatch ?? {}
175
+ return () => {
176
+ if (texts.length > 0) return { ...patch, content: texts.map((item) => ({ type: 'text', text: item })) }
177
+ return null
178
+ }
179
+ }
180
+ default: {
181
+ // 模板专属策略:懒加载 <strategyDir>/<strategy>.mjs,模块约定导出
182
+ // `createResolver(config)` 或默认导出同名工厂。加载失败按单配置失败语义
183
+ // 抛给调用方(executor 会跳过该配置并 warnOnce)。
184
+ if (typeof strategyDir !== 'string' || strategyDir.length === 0) {
185
+ throw new TypeError(`${name}: unknown config strategy ${JSON.stringify(config.strategy)}`)
186
+ }
187
+ const moduleUrl = new URL(`${config.strategy}.mjs`, strategyDir.endsWith('/') ? strategyDir : `${strategyDir}/`)
188
+ let loaded
189
+ return async (args) => {
190
+ if (loaded === undefined) {
191
+ loaded = await import(moduleUrl.href)
192
+ }
193
+ const make = typeof loaded.createResolver === 'function'
194
+ ? loaded.createResolver
195
+ : typeof loaded.default === 'function'
196
+ ? loaded.default
197
+ : undefined
198
+ if (typeof make !== 'function') {
199
+ throw new TypeError(`${name}: strategy module ${moduleUrl.href} must export createResolver(config)`)
200
+ }
201
+ return make(config)(args)
202
+ }
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,54 @@
1
+ import { labelOf, toolNameSet } from './shared.mjs'
2
+
3
+ export function prepareDecision(action, plugin) {
4
+ const label = labelOf(action)
5
+ const names = toolNameSet(action.toolNames, plugin, `${label}.toolNames`)
6
+ const matchesTool = (exec) => names === undefined || names.has(exec?.name)
7
+ const hit = (exec, result) => typeof action.match !== 'function' || action.match(exec, result) === true
8
+ const phase = action.phase === undefined ? 'pre' : action.phase
9
+ if (!['pre', 'post'].includes(phase)) {
10
+ throw new TypeError(`${plugin}: ${label}.phase must be one of pre, post`)
11
+ }
12
+ if (phase === 'pre') {
13
+ const decision = action.decision ?? 'allow'
14
+ if (!['allow', 'deny', 'ask'].includes(decision)) {
15
+ throw new TypeError(`${plugin}: ${label}.decision must be one of allow, deny, ask`)
16
+ }
17
+ return (_ctx, { warnOnce, on, collect, take }) => collect(on('tools/pre-execute', async (exec, next) => {
18
+ try {
19
+ if (!matchesTool(exec) || !hit(exec)) return next()
20
+ if (decision === 'allow') return next()
21
+ const outcome = decision === 'deny'
22
+ ? { kind: 'deny', reason: String(action.reason ?? `${label}: denied by action`) }
23
+ : { kind: 'ask' }
24
+ return take(exec) ? outcome : next()
25
+ } catch (error) {
26
+ warnOnce(`${plugin}: decision(pre) action ${label} failed: ${String(error?.message ?? error)}`)
27
+ return next()
28
+ }
29
+ }))
30
+ }
31
+ const postAction = action.action ?? 'accept'
32
+ if (!['accept', 'replace', 'block'].includes(postAction)) {
33
+ throw new TypeError(`${plugin}: ${label}.action must be one of accept, replace, block`)
34
+ }
35
+ const text = typeof action.text === 'string' ? action.text : ''
36
+ return (_ctx, { warnOnce, on, collect, take }) => collect(on('tools/post-execute', async (exec, result, next) => {
37
+ try {
38
+ if (!matchesTool(exec) || !hit(exec, result)) return next()
39
+ if (postAction === 'accept') return next()
40
+ if (postAction === 'replace' && text.length > 0) {
41
+ if (!take(exec)) return next()
42
+ return { kind: 'accept', content: [{ type: 'text', text }] }
43
+ }
44
+ if (postAction === 'block') {
45
+ if (!take(exec)) return next()
46
+ return { kind: 'block', feedback: [{ type: 'text', text: text.length > 0 ? text : `${label}: blocked by action` }] }
47
+ }
48
+ return next()
49
+ } catch (error) {
50
+ warnOnce(`${plugin}: decision(post) action ${label} failed: ${String(error?.message ?? error)}`)
51
+ return next()
52
+ }
53
+ }))
54
+ }
@@ -0,0 +1,11 @@
1
+ export const ACTION_EXAMPLES = {
2
+ 'inject-text': { config: { id: 'notice', layer: 'pre-step', text: '' } },
3
+ assembly: { target: {} },
4
+ decision: { phase: 'pre', decision: 'allow' },
5
+ 'append-context': { mode: 'context', text: '' },
6
+ guard: { mask: { deny: [] } },
7
+ 'sdk-strip': { mask: { deny: [] } },
8
+ 'request-params': { patch: {} },
9
+ 'inbox-prepend': { target: 'next-turn', text: '' },
10
+ 'pre-step-filter': { blockPlugins: [] },
11
+ }
@@ -0,0 +1,80 @@
1
+ import { labelOf, createMask, RUN_CODE } from './shared.mjs'
2
+
3
+ export function prepareGuard(action, plugin) {
4
+ const label = labelOf(action)
5
+ const mask = createMask(action.mask ?? action, `${label}.mask`, plugin)
6
+ const includeSubagents = action.includeSubagents === true
7
+ const reasonFor = (toolName) => String(action.reason ?? `${label}: tool ${JSON.stringify(toolName)} is denied by action`)
8
+
9
+ return (_ctx, { warnOnce, on, collect }) => {
10
+ /** session -> { ctx, disposers }:同一 agent.ctx 只注册一次,重绑后按新 ctx 重注册。 */
11
+ const appliedBySession = new WeakMap()
12
+ const states = new Set()
13
+ const releaseState = (state) => {
14
+ states.delete(state)
15
+ for (const dispose of state.disposers.splice(0)) {
16
+ try { dispose() } catch { /* 释放失败不得反过来打断卸载 */ }
17
+ }
18
+ }
19
+ const releaseAll = () => { for (const state of [...states]) releaseState(state) }
20
+
21
+ const applyTo = (agent) => {
22
+ const session = agent?.session
23
+ const scoped = agent?.ctx
24
+ if (session === undefined || scoped === undefined) return
25
+ // 受众不在本动作的裁决面内就不注册(子代理的 guard 由它自己的 assembly 决定;
26
+ // 万一父 guard 经 scope 链被走到,guard 体内的同一道受众判定仍然放行)。
27
+ const depth = session.header?.delegationDepth ?? 0
28
+ if (depth > 0 ? !includeSubagents : action.audience === 'subagent') return
29
+ const existing = appliedBySession.get(session)
30
+ if (existing !== undefined) {
31
+ // 预设切换/重绑会换掉 agent.ctx:旧 scope 的 guard 随旧 scope 失效,必须重注册。
32
+ if (existing.ctx === scoped) return
33
+ releaseState(existing)
34
+ appliedBySession.delete(session)
35
+ }
36
+ const tools = scoped.tools
37
+ if (tools === undefined || typeof tools.guard !== 'function') {
38
+ warnOnce(`${plugin}: guard action ${label} found no agent-scoped tools service — execution guard not registered`)
39
+ return
40
+ }
41
+ const state = { ctx: scoped, disposers: [] }
42
+ states.add(state)
43
+ appliedBySession.set(session, state)
44
+ const denials = {
45
+ ...(mask.allow !== undefined ? { allow: [...mask.allow] } : {}),
46
+ ...(mask.deny !== undefined ? { deny: [...mask.deny] } : {}),
47
+ }
48
+ // restrict 沿父 scope 链传播且不识别受众;只在子代理也受限时复用。
49
+ if (includeSubagents && typeof tools.restrict === 'function') {
50
+ try {
51
+ const dispose = tools.restrict(denials)
52
+ if (typeof dispose === 'function') state.disposers.push(dispose)
53
+ } catch (error) {
54
+ // restrict 只收继承面:本层/晚到工具不在它的可限制集合里,交给 guard 裁决。
55
+ warnOnce(`${plugin}: guard action ${label} restrict skipped (own-layer/late tools stay covered by the guard): ${String(error?.message ?? error)}`)
56
+ }
57
+ }
58
+ state.disposers.push(tools.guard((exec) => {
59
+ const toolName = exec?.name
60
+ if (toolName === RUN_CODE || typeof toolName !== 'string' || toolName.length === 0) return undefined
61
+ const depth = exec?.agent?.session?.header?.delegationDepth ?? 0
62
+ // 主子代理隔离:scope 注册之外再按受众判定一次(子代理 scope 可能挂在父链上)。
63
+ if (depth > 0 ? !includeSubagents : action.audience === 'subagent') return undefined
64
+ return mask.blocks(toolName) ? reasonFor(toolName) : undefined
65
+ }))
66
+ }
67
+
68
+ collect(releaseAll)
69
+ collect(on('system-prompt/assemble', async (assembly, context, next) => {
70
+ // Downstream errors propagate untouched; only this action's own logic is guarded.
71
+ const assembled = await next()
72
+ try {
73
+ applyTo(context?.agent)
74
+ } catch (error) {
75
+ warnOnce(`${plugin}: guard action ${label} failed to register: ${String(error?.message ?? error)}`)
76
+ }
77
+ return assembled
78
+ }))
79
+ }
80
+ }
@@ -0,0 +1,37 @@
1
+ import { labelOf, requireString } from './shared.mjs'
2
+ import { newMessageId } from '../shared.mjs'
3
+
4
+ export function prepareInboxPrepend(action, plugin) {
5
+ const label = labelOf(action)
6
+ const target = action.target ?? 'next-turn'
7
+ if (target !== 'next-turn' && target !== 'next-step') {
8
+ throw new TypeError(`${plugin}: ${label}.target must be "next-turn" or "next-step"`)
9
+ }
10
+ const text = requireString(action.text, `${label}.text`, plugin)
11
+ // 空正文 = 没有可插入的内容(empty 情形):不注册,而不是插入一条空消息。
12
+ if (text.length === 0) return
13
+ return (_ctx, { warnOnce, on, collect, take }) => collect(on('agent/inbox/inserted', ({ agent, message } = {}) => {
14
+ try {
15
+ // 无 session 的 agent 不锚定(与原 `anchor-turn` 的守卫逐条对齐;声明侧见
16
+ // `test/engine/declarations/anchor-turn.yml` 的 session 谓词):锚定是**会话**
17
+ // 首轮语义,没有会话就没有「首轮」;缺这条会让匿名/临时 agent 也被插一条合成消息
18
+ // (对拍实测:声明路径多插 2 条)。
19
+ if (agent?.session === undefined) return
20
+ if (typeof agent?.inbox?.prepend !== 'function') return
21
+ // v4 起插件来源的 kind 是生产者名(`plugin:<name>`);两种形态一起认,否则换代后
22
+ // 「插件来源消息永不再次前置」这道防自触发的闸会静默失效。
23
+ if (typeof message?.source?.kind === 'string' && message.source.kind.startsWith('plugin:')) return
24
+ if (message?.source?.kind === 'plugin') return
25
+ if (typeof action.match === 'function' && action.match(agent, message) !== true) return
26
+ if (!take({ agent })) return
27
+ agent.inbox.prepend(target, {
28
+ id: newMessageId(`action-${label}`),
29
+ role: 'user',
30
+ content: [{ type: 'text', text }],
31
+ source: { kind: `plugin:${label}`, plugin: label, form: 'notice', summary: `${label} ${target}` },
32
+ })
33
+ } catch (error) {
34
+ warnOnce(`${plugin}: inbox-prepend action ${label} failed: ${String(error?.message ?? error)}`)
35
+ }
36
+ }))
37
+ }
@@ -0,0 +1,100 @@
1
+ import { createWarnOnce, keepDisposer } from '../shared.mjs'
2
+ import { labelOf } from './shared.mjs'
3
+ import { ACTION_KINDS } from './catalog.mjs'
4
+ import { actionSupportsWhen, actionExecutionPoint, createTurnBudget, withWhen, channelBinder } from './registration.mjs'
5
+ import { prepareInjectText } from './inject-text.mjs'
6
+ import { prepareAssembly } from './assembly.mjs'
7
+ import { prepareDecision } from './decision.mjs'
8
+ import { prepareAppendContext } from './append-context.mjs'
9
+ import { prepareGuard } from './guard.mjs'
10
+ import { prepareSdkStrip } from './sdk-strip.mjs'
11
+ import { prepareRequestParams } from './request-params.mjs'
12
+ import { prepareInboxPrepend } from './inbox-prepend.mjs'
13
+ import { preparePreStepFilter } from './pre-step-filter.mjs'
14
+ export { ACTION_KINDS, ACTION_DEGRADE } from './catalog.mjs'
15
+ export { actionSupportsWhen, actionExecutionPoint } from './registration.mjs'
16
+ const DEFAULT_PLUGIN = 'prompt-actions'
17
+
18
+ const PREPARERS = {
19
+ 'inject-text': prepareInjectText,
20
+ assembly: prepareAssembly,
21
+ decision: prepareDecision,
22
+ 'append-context': prepareAppendContext,
23
+ guard: prepareGuard,
24
+ 'sdk-strip': prepareSdkStrip,
25
+ 'request-params': prepareRequestParams,
26
+ 'inbox-prepend': prepareInboxPrepend,
27
+ 'pre-step-filter': preparePreStepFilter,
28
+ }
29
+
30
+ /**
31
+ * 注入整批编译与动作准备共用选项校验;不装配服务,也不单独编译注入叶子。
32
+ *
33
+ * @param action `{ kind, id?, ... }`,形状见 {@link ACTION_KINDS}。
34
+ * @param options.plugin 调用方插件名(错误消息 / 告警 / disposer 标签),与
35
+ * `mountTriggers(ctx, declarations, { plugin })` 同一约定;缺省 `prompt-actions`。
36
+ * @param options.warnOnce 复用宿主模块的告警器;缺省为本文件自己的 warnOnce。
37
+ * @param options.when 可选判定:不命中(非 `true`)即放行下游、不执行本动作。仅对
38
+ * {@link ON_REGISTERED_KINDS} 里的动作有效;对 `inject-text` / `guard` 传它会挂载期报错
39
+ * (那两类不走 `on(...)`,静默忽略就是"配了没效果")。
40
+ * @param options.prepend 让本动作落在宿主 waterfall 的**最外层**(否决型动作需要)。同样只对
41
+ * `ON_REGISTERED_KINDS` 有效;它表达的是**位置**,不承担声明之间的排序。
42
+ * @param options.promptConfigOptions 宿主提供的 createPromptConfigs 选项(模板基准和策略目录),不从动作数据读取。
43
+ * @returns 已校验的动作类别与调用方插件名。
44
+ */
45
+ export function validateActionOptions(action, options = {}) {
46
+ const kind = action?.kind
47
+ const plugin = typeof options.plugin === 'string' && options.plugin.length > 0 ? options.plugin : DEFAULT_PLUGIN
48
+ if (!Object.hasOwn(ACTION_KINDS, kind)) {
49
+ throw new TypeError(`${plugin}: unknown action kind ${JSON.stringify(kind)} — known kinds: ${Object.keys(ACTION_KINDS).join(', ')}`)
50
+ }
51
+ if (!actionSupportsWhen(kind) && (options.when !== undefined || options.prepend === true)) {
52
+ const feature = options.when !== undefined ? 'a when predicate' : 'prepend'
53
+ throw new TypeError(`${plugin}: action ${kind} does not support ${feature} — 它不经 on(...) 注册(inject-text 注册层、guard 注册 agent scope 的最终拒绝)`)
54
+ }
55
+ // `maxPerTurn` 与 `when` 的支持面相同:都只对经 `on(...)` 注册的动作有效。
56
+ if (!actionSupportsWhen(kind) && action?.maxPerTurn !== undefined) {
57
+ throw new TypeError(`${plugin}: action ${kind} does not support maxPerTurn — 它不经 on(...) 注册`)
58
+ }
59
+ const max = action?.maxPerTurn
60
+ if (max !== undefined && (!Number.isSafeInteger(max) || max <= 0)) {
61
+ throw new TypeError(`${labelOf(action)}.maxPerTurn must be a positive integer`)
62
+ }
63
+ return { kind, plugin }
64
+ }
65
+
66
+ /** 纯准备阶段返回真实 ctx 的绑定函数;options.on 可由统一规则收集实际执行点。 */
67
+ export function prepareAction(action, options = {}) {
68
+ const { kind, plugin } = validateActionOptions(action, options)
69
+ const max = action?.maxPerTurn
70
+ const bind = PREPARERS[kind](action, plugin, options.promptConfigOptions)
71
+ const phase = actionSupportsWhen(kind) ? actionExecutionPoint(action).phase : undefined
72
+ return (ctx) => {
73
+ const warnOnce = options.warnOnce ?? createWarnOnce(ctx, plugin)
74
+ const budget = createTurnBudget(max)
75
+ const on = withWhen(options.on ?? channelBinder(ctx, kind, plugin, options.prepend === true), options.when, warnOnce, phase)
76
+ const disposers = []
77
+ const collect = (disposer) => { if (typeof disposer === 'function') disposers.push(disposer) }
78
+ // 每轮预算的轮边界由 durable 事件驱动(与谓词的 observe 同一事件源,但**独立记账**:
79
+ // 预算是「本动作生效次数」,不是 durable 事件计数)。
80
+ if (budget !== undefined) collect(ctx.on('session/event', (session, event) => budget.observe(session, event)))
81
+ const take = (subject) => budget?.take(subject) ?? true
82
+ bind?.(ctx, { warnOnce, on, collect, take })
83
+ const dispose = () => {
84
+ for (const release of disposers.splice(0)) {
85
+ try {
86
+ release()
87
+ } catch {
88
+ // 释放失败不得反过来打断卸载流程。
89
+ }
90
+ }
91
+ }
92
+ keepDisposer(ctx, dispose, `${plugin}: action ${labelOf(action)}`)
93
+ return dispose
94
+ }
95
+ }
96
+
97
+ /** 校验后把动作接到其合法通道,返回释放函数。 */
98
+ export function registerAction(ctx, action, options = {}) {
99
+ return prepareAction(action, options)(ctx)
100
+ }
@@ -0,0 +1,22 @@
1
+ import { applyPromptConfigs } from '../executor.mjs'
2
+ import { wireLayers } from '../layers.mjs'
3
+ import { createPromptConfigs } from '../schema.mjs'
4
+
5
+ export function prepareInjectText(action, plugin, promptConfigOptions) {
6
+ const source = action.config
7
+ if (source === null || typeof source !== 'object' || Array.isArray(source)) {
8
+ throw new TypeError(`${plugin}: inject-text requires a config object (与 promptConfigs 同形状的提示词配置)`)
9
+ }
10
+ if (typeof source.layer !== 'string' || source.layer.length === 0) {
11
+ throw new TypeError(`${plugin}: inject-text config.layer is required (九层之一)`)
12
+ }
13
+ // 纯声明复用提示词配置编译;函数仅来自已有编程接口,不写回可序列化声明。
14
+ const config = typeof source.resolve === 'function' ? source : createPromptConfigs([source], promptConfigOptions)[0]
15
+ return (ctx, { warnOnce, collect }) => {
16
+ if (config.layer === 'pre-step') {
17
+ collect(applyPromptConfigs(ctx, [config], action.options ?? {}))
18
+ return
19
+ }
20
+ collect(wireLayers(ctx, [config], warnOnce))
21
+ }
22
+ }