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
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > 一切皆可注入:把 DSH 官方开放的全部注入层级收敛为一个可配置提示词注入引擎——注入什么、注入到哪一层、何时注入,全由提示词配置决定。
4
4
 
5
- DSH 生态的提示词注入层:`prompt-config-engine.mjs` 对接九个官方插入点,把可叠加的注入方案保存在各自模块中。包内模块库提供复制起点;模块不注册为官方会话预设。
5
+ DSH 生态的提示词注入层:`rule-engine.mjs` 按「条件触发 → 执行动作」对接九个官方插入点。每张配置卡对应一条独立规则,可组合多条件与多动作;提示词注入也是动作。包内模块库提供复制起点;模块不注册为官方会话预设。
6
6
 
7
7
  > 能力来源:工具目录锚定与晋升门控移植自 [dsh-anchored-standard](https://github.com/xiaobright/dsh-anchored-standard)(MIT,上游已于 2026-09-10 冻结),近距离引导参考 [dsh-router-standard](https://github.com/yjh051108/dsh-router-standard),缓存铁律参考 [dsh-super-injector](https://github.com/yjh051108/dsh-super-injector)。**本项目只移植引擎能力,不再分发上游预设**:锚定/深思链路由模块与规则按需声明。
8
8
 
@@ -28,7 +28,18 @@ dsh --profile prompt-tool
28
28
 
29
29
  ### 当前格式与宿主要求
30
30
 
31
- 模块行为存于 `module.yml`。当前公开共享参数与旧规则快捷参数分开管理:旧快捷参数只经统一适配器投影到规则,保存规则时清理已有承接的旧键;无承接的值保留并告警。
31
+ module.yml 保存模块完整定义,支持 UI 自动保存和直接手改完整文件;rules/ 是校验后供 UI 与运行时使用的切片,手改切片或校验失配会从完整定义恢复。旧 promptConfigs、triggers、模型路由与锚定快捷参数须先离线迁移;运行时不自动双读。共享能力参数归 layerSettings,指令正文保持独立权限通道。
32
+
33
+ 升级旧模块前,由用户停止相关 DSH 服务,再使用安装包提供的 `prompt-tool-migrate-rules`,或在已构建仓库中执行:
34
+
35
+ ```powershell
36
+ $Repo = 'D:/AI/GitHub/dsh-plugin-prompt-tool'
37
+ pnpm --dir $Repo migrate:rules -- --root '<DSH_HOME>/.prompt-tool/modules' --characters-root '<DSH_HOME>/.prompt-tool/.characters' --check
38
+ pnpm --dir $Repo migrate:rules -- --root '<DSH_HOME>/.prompt-tool/modules' --characters-root '<DSH_HOME>/.prompt-tool/.characters' --apply
39
+ # 需要恢复时,保持服务停止,并用相同目录执行 --rollback
40
+ ```
41
+
42
+ 目录必须显式指定为存在的绝对路径;没有角色库时省略 `--characters-root`。先全量预检,再生成完整候选、校验来源版本并原子替换;原文件保留在各根的 `.rules-migration-backup`。重复应用无改动,回滚只覆盖仍与迁移产物一致的目录。无法证明等价的手写组合、互斥多启用冲突、失效旧参数或自定义策略目录会拒绝迁移,要求先整理。迁移完成后由用户重新启动 DSH。
32
43
 
33
44
  技能调用策略只接受官方 frontmatter 键,状态文件只接受 v4;不提供旧布局迁移、回滚或备份脚本。
34
45
 
@@ -43,31 +54,31 @@ dsh --profile prompt-tool
43
54
  - 🔌 **九个官方插入点按需接线**:一个引擎接入声明所需的插入点,共享同一套过滤与降级语义
44
55
  - ✍️ **一切皆可配置**:`layer / strategy / position / promotion / audience / modelScope / mergeMode / order / text / texts / fill / variables / params` 全开放
45
56
  - 🧑‍🤝‍🧑 **消息受众三态**:`audience: main / subagent`,省略 `audience` 表示公用;身份类提示词可只注入子代理
46
- - 🗂️ **内容与执行分离**:每条提示词配置物化为 `~/.dsh/.prompt-tool/modules/<模块>/configs/` 下的 yml;`module.yml.configOrder` 保存轻量序号,文件名前缀由它生成
47
- - 🧩 **三层合并**:引擎默认(按 params 生成)< 模板默认 promptConfigs < 预设 promptConfigs,同名 `id` 覆盖
57
+ - 🗂️ **完整定义与校验切片**:module.yml 保存完整行为,rules/<id>.yml 保存正文,_settings.yml 保存顺序和启停,variables.yml 保存模板变量;失配单向恢复,不生成旧宿主装配产物。
58
+ - 🧩 **显式互斥**:同模块同组中任一卡声明互斥时,启用目标卡会原子关闭同组其他卡的总开关;重排不改变启用状态。
48
59
  - 🖥️ **可拖动悬浮工作台入口**:工作台经官方 `shell.overlay` 渲染悬浮触发器与 body portal 抽屉;按钮可拖动、位置存插件自己的 localStorage、窗口变化自动夹回可见区(不读宿主布局树,已移除 `sidebar.footer.action` 几何探针);五页(主会话/子代理/工具预览/技能设置/模块)在抽屉内渲染,抽屉用 fixed + z-index 置顶,不被宿主导航栏遮挡
49
- - 🧪 **六种内容策略**:`static / first-turn-anchor / guide-auto / custom-fallback / placeholder / world-book`(world-book 支持 ST selectiveLogic 选择性触发:任一/副键全中/排除)
60
+ - 🧪 **条件与内容分离**:`when` 支持组合判断;注入动作支持 `static / first-turn-anchor / guide-auto / anchor-notice / placeholder / world-book`。旧 `custom-fallback` 拆为锚点条件与通知内容。
50
61
  - 🛡️ **失败不伤会话**:单条失败跳过 + `warnOnce`;配置错误挂载时 fail loud;`dedupe: session` 持久幂等
51
62
  - 🧭 **通用 instruction-hint 引擎**:模块可通过 `strategy: placeholder` 与 `fill: instruction-hint` 提示指令文件存在;实现位于 `engine/instruction-hint.mjs`。`layerSettings.pre-step.instructionHint` 可启用该能力,按模型可见 surface 去重,重挂不重复,被压缩遮蔽后才再次提示
52
63
  - 📦 **Bridge 载荷**:JSON 请求统一 32 MiB 硬上限并明确返回 413;角色卡原始图片走 64 MiB 流式通道,按 PNG 魔数识别。
53
64
  - 📂 **技能管理**:官方发现与会话快照统一技能来源、生效和遮蔽状态;单端开关写回技能文件。支持目录包与直属 Markdown 技能、创建、两种复制导入,以及用户根和引用根的可恢复删除;技能局部刷新保留其他页面草稿。
54
65
  - 🎭 **SillyTavern 导入**:JSON 预设、角色卡和独立世界书转换为本地预设——按官方顺序表保留启停,赋值模板运行时求值;不等价能力明确报告,采样参数由宿主管理
55
- - 🎴 **角色卡库**:PNG/JSON/YAML 角色卡与原生角色片段经统一预览后逐张入库;PNG 保留原图,更新保留角色记忆,按需应用到当前预设。
66
+ - 🎴 **角色卡导入**:PNG/JSON/YAML 角色来源经统一预览后成为普通模块;PNG 保留原图,更新保留模块记忆与用户资产。
56
67
  - 📦 **预设交换**:文件夹、ZIP、原生 JSON/YAML 与 ST 来源共用识别、预览和完整候选安装;导出可选完整 ZIP 或仅定义 YAML。资源、覆盖及分享边界见 [资产交换文档](docs/asset-transfer.md)。
57
68
  - 📚 **世界书**:`character_book` 转 world-book 策略配置(`keys` 命中触发 / `constant` 常驻 / 正则键自动检测 / `selectiveLogic` 组合逻辑),与模块卡片同一存储与编辑(模块列表「世界书」过滤 + 批量启用/禁用)
58
69
  - 🛠️ **自定义工具**:module.yml `customTools` 段声明式定义模型工具(执行器 shell/http/delegate/fs/ask-user,`{{args.x}}` 参数插值);参数与输出经官方 `dsh-tools` 转换器物化为标准 JSON Schema,非法参数产生标准工具错误,delegate 经 `ctx.tools.execute` 嵌套调度走完整官方工具管线;`customTools.scope` 暂不支持(显式拒绝)
59
70
  - 🛡️ **子代理工具策略**:module.yml 顶层 `subagentToolPolicy` 段(opt-in)声明 ceiling、profiles、角色卡绑定、有序任务规则与受控模型扩权;`subagent` 固定走 spawn、`subagent_fork` 固定走 fork,按官方 `SubagentRun`/continuable 契约创建并在窗口内冻结 toolFilter;模型选择器和扩权参数在工具 body 前校验,模型路由经过 LLM preflight;UI 从官方 sessions snapshot 读取当前会话,并可编辑、停用、预览策略及查询存活 Agent 工具面
60
71
  - ♻️ **Session 日志读取**:引擎冷启动与幂等扫描统一走官方 `session.snapshotEvents()`(DSH `0.1.2-alpha.4+`),不再读取已移除的 `session.events` 数组。
61
- - 🧩 **模板变量**:仅从预设顶层 `variables` 段提供 `{{key}}` 插值默认值,单条提示词配置的 `variables` 可局部覆盖——模块列表顶部「模板变量」卡片统一编辑(可折叠/清空/停用/失焦自动保存)。`params` 中的旧内容变量及 `params.variables` 不再读取,也不自动迁移;旧预设需自行整理到顶层后重新物化。锚定匹配引擎(anchor-match)统一 custom-fallback 与 world-book 的匹配语义
72
+ - 🧩 **模板变量**:顶层 `variables` 提供 `{{key}}` 插值,注入动作的 `config.variables` 可局部覆盖。正文、匹配词、分类阈值与模型偏好由规则或模板显式提供;业务配置为空就不生成对应内容,引擎不内藏默认锚句。
62
73
  - 💬 **会话变量工具**:`session_var`(list/get/set/clear)——模型维护角色状态(`{{心情}}` 等),会话级覆盖预设默认;ST 运行时宏(`{{lastusermessage}}` / `{{lastcharmessage}}`)从会话事件提取
63
74
  - 🧩 **工具按模块装配**:角色卡、世界书、会话变量、自定义工具分别由 `character-tools` / `world-book-tools` / `session-var-tools` / `tool-config-engine` 模块提供;不再维护重复的顶层工具开关
64
- - 📐 **显式按需装配**:空模块不自动附加增强能力;模块人设来自顶层 `persona`,官方工具与普通委派仍由会话原有预设提供。首轮门控、来源过滤、工具名单、深思门与进度节拍可由模块顶层 `triggers` 按需声明,物化为 `triggers.yml` 后由 `declared-triggers` 读入;声明错误在保存或挂载期明确报告。
75
+ - 📐 **显式按需装配**:空模块不自动附加增强能力;模块人设来自顶层 `persona`,官方工具与普通委派仍由会话原有预设提供。首轮门控、来源过滤、工具名单、深思门与进度节拍统一由 `rules` 声明;总入口负责编译调度,`engine/conditions/` 判断,`engine/actions/` 执行。
65
76
 
66
77
  ## Web 客户端结构
67
78
 
68
- 九层提示词配置卡展开后,使用“条件 / 执行 / 内容 / 设置”分栏编辑,窄屏自动改为标签页。页面切换保留未保存草稿;设置视图修改同层共享配置,不复制为每条提示词的局部参数。
79
+ 每条配置保留一张卡,复用原卡壳、折叠箭头和拖拽样式,指令文件卡置顶。卡内通过“条件 / 动作 / JSON / 设置”平级侧边导航编辑,窄屏改为横向页签,正文归对应注入动作。上移、下移与拖拽按钮位于卡头总开关旁。Linear 风格统一桌面控件为 28px、按钮与下拉为胶囊;短输入按字符宽度收紧,下拉按最长选项文案限宽,数字 96px,正文与 JSON 保留完整编辑宽度。粗指针的 44px 触控目标扩大控件本体,不覆盖外部空白。
69
80
 
70
- 声明规则编辑器读写预设顶层 `triggers`,提供条件和动作编辑、完整声明、校验与版本冲突保护。支持七类判断原语、四种组合与九类动作;`inject-text`、`guard` 不接受通用顶层 `when`,界面会提示限制。具体支持范围见 [引擎复用指南](docs/engine-reuse.md#声明的条件与动作边界)。
81
+ 规则编辑器通过 `/rules` 保存局部修改与完整文件版本,保留在途新输入、未知字段和未完成 JSON 草稿。动态注入支持 `when`;固定注册的独占、抑制及安全守卫拒绝不支持的动态条件。具体支持范围见 [引擎复用指南](docs/engine-reuse.md#声明的条件与动作边界)。
71
82
 
72
83
  Web 客户端按四层组织:
73
84
 
@@ -89,7 +100,7 @@ src/client/
89
100
  - 五页工作台保留搜索、展开和滚动位置;工具、人设、策略以及非法 JSON/数字输入可跨页继续编辑。存在未保存草稿时,模块切换会提示先处理,不会无声覆盖。
90
101
  - 配置保存与校验位于长列表操作区,失败就近显示;低频卡片操作收进菜单,删除和丢弃文件草稿需明确确认。窄容器自动换行,键盘操作、明暗主题和减弱动效均沿用宿主规范。
91
102
 
92
- - 参数在模块列表的配置卡内编辑。引擎能力按实际 `modules` 装配显示,一项已装配能力一张卡,直接编辑自身参数;删除只移除该能力的模块声明。共享参数定义继续拥有字段、校验、默认草稿、保存快照和组合行映射。主/子代理模型保留专用配置卡。
103
+ - 参数在所属规则的动作中编辑,主/子代理模型参数使用 `request-params` 动作与受众条件。共享能力仍按实际 `modules` 装配显示,当前会话模型继续使用官方选择控件。
93
104
  - 「指令提示」统一通过「前置步骤 → 内容策略:动态填充 → 填充来源:指令提示」编辑,不再作为独立策略入口。指令文件正文和授权保存仍归独立文件通道,不复制进预设。
94
105
  - AGENTS.md / CLAUDE.md 这类指令文件复用普通配置卡组件,保留名称、路径、正文和后续官方注入的启停;位置、顺序、晋升、受众与模型范围由官方负责,不提供文件卡控制项。名称和启停落独立指令策略,正文就地编辑、**焦点离开卡片时自动写回原文件**(版本冲突保留草稿并提供「重新读取」,没有单独的保存按钮)。
95
106
  - 创建菜单始终提供全部层级模板、工具和可添加能力,不受当前列表筛选限制。创建后只展开目标卡片(当它落在当前筛选视野内),不改动层级筛选与搜索词;同一提示词模板可重复创建,自动分配不重复标识。空内容不等于删除,保存和后台刷新保留未完成草稿;工具草稿在层级/世界书筛选往返时不丢失。
@@ -110,9 +121,9 @@ src/client/
110
121
 
111
122
  ## 模块参数体系
112
123
 
113
- 初始化从包内 `modules/` 补建缺失的同名模块,已有目录不覆盖。保存与重建统一通过 `materializeModule` 读取目标模块自己的定义和正文,不借用工作台另一个模块的运行时副本。
124
+ 初始化从包内 modules/ 补建缺失模块,已有目录不覆盖。包内内置模块三个:`ponytail`(懒惰资深工程师行为规则)、`tool-surface`(工具面收窄到常驻核心集,其余按需解锁)、`skill-surface`(拦掉全量技能目录注入,改按需发现)——后两个默认不启用,需在「模块」页启用并重启。materializeModule 原地校验恢复 rules/,只清理已知旧产物,保留记忆、技能、正文与用户资产。工具和子代理策略从完整定义内联装配。
114
125
 
115
- 部署 Config 只保留 `writePreset`,含义是模块运行总闸:关闭仅撤回运行贡献,不清空文件。`presetOrder`、`fallbackText` 和 `presetTemplate` 不再是部署设置;编辑目标通过请求头 `x-module-id` 传递,不切换或跟随官方会话预设。
126
+ 部署 Config 使用 modulesEnabled 作为模块运行总闸:关闭仅撤回贡献,不清空文件。旧 writePreset 仅作输入兼容,双键冲突拒绝。编辑目标通过 x-module-id 传递,不切换或跟随官方会话预设。
116
127
 
117
128
  模块行为由一份 `module.yml` 下发,参数所有者各自独立:
118
129
 
@@ -120,22 +131,20 @@ src/client/
120
131
  |---|---|
121
132
  | `layerSettings.<层名>` | 模块共享参数;磁盘归属由参数目录的 `storageLayer` 固定,不随 UI 分组改变 |
122
133
  | `moduleConfigs` | 行级 config 直写通道(参数桥未覆盖的键:超时/环境白名单/ST 导入等),不锁定覆盖 UI 可管理参数 |
123
- | `promptConfigs` | 独立命名的注入规则,`params` 仅属于该规则;与预设默认及生成配置按 id 合并 |
124
- | `configOrder` | 配置 ID 到序号的轻量映射;正文仍属于各自配置,文件名前缀由此物化 |
134
+ | `rules` | 独立身份、条件树 `when` 与动作数组 `do`;注入正文与参数归相应动作 |
135
+ | `configOrder` | 规则 ID 到序号的完整定义映射;运行切片中为 _settings.yml.rules[id].order,文件名不带序号 |
125
136
 
126
137
  ### 共享参数一览(全部可选,缺省按对应模块解释)
127
138
 
128
139
  | 分类 | 键 |
129
140
  |---|---|
130
- | 主模型 | `modelProvider` `modelName` `modelReasoningEffort` `modelTemperature` `modelMaxTokens` |
131
- | 子模型 | `subagentModelProvider` `subagentModelName` `subagentReasoningEffort` `subagentTemperature` `subagentMaxTokens` |
132
141
  | 策略深度 | `maxDepth`,只约束已启用的插件子代理工具策略;普通官方委派由宿主管理 |
133
142
  | 指令提示 | `instructionHint`,默认关闭 |
134
143
  | 工具 | `toolGitBashEnabled` `customToolRequireApproval` |
135
144
 
136
- 以上共 14 个公开共享参数。锚定、引导及正文注入的 15 个旧快捷键由 `host/legacy-prompt-params.ts` 读取兼容,投影到 `near-anchor`、`router-guide`、`prompt-injector` 规则;新编辑器直接编辑规则。成功保存规则时清理已有承接的旧键,无承接的旧值保留并告警,旧 `fieldSources` 不再锁住这些规则字段。编辑器输出上限 `strReplaceEditorMaxOutputChars` 已退出插件参数面,由宿主工具配置负责。
145
+ 以上共 4 个公开共享参数。模型参数属于 `request-params` 动作,主/子代理由条件区分;旧模型键与 15 个锚定/引导快捷键只供离线迁移读取,不再提供在线保存入口。无法等价承接的旧值明确拒迁,不丢弃也不静默激活。编辑器输出上限 `strReplaceEditorMaxOutputChars` 由宿主工具配置负责。
137
146
 
138
- > 首轮工具面与输出封顶、`stages` 式阶段窄化、pre-step 来源名单、常驻工具白/黑名单、锚句、深思门与进度节拍都没有共享参数键:它们改由预设顶层 `triggers` 段声明(示例见 [engine 复用指南](docs/engine-reuse.md))。其中 `stages` 阶段窄化与「晋升后才切 PTC 呈现」是本轮的两项净损失;子代理工具面只能由 `subagentToolPolicy` 实例策略授权。
147
+ > 首轮工具面与输出封顶、pre-step 来源名单、常驻工具白/黑名单、锚句、深思门与进度节拍通过 `rules` 声明(示例见 [engine 复用指南](docs/engine-reuse.md));子代理工具面仍由 `subagentToolPolicy` 实例策略授权。
139
148
 
140
149
  AGENTS.md 走「文件即真相」:文件集合、正文与版本不物化进生成目录,而是由宿主按本会话工作区现场解析(`$DSH_HOME/AGENTS.md` + 工作区 cwd→项目根链的 AGENTS.md/CLAUDE.md/AGENTS.local.md/CLAUDE.local.md);工作台中的文件卡指向原文件,卡片定义与正文都不进 `module.yml`。插件不写常驻受管块。
141
150
 
@@ -176,7 +185,7 @@ persona:
176
185
 
177
186
  模块编辑选择与官方会话预设独立。切换编辑模块只改变请求目标;未指定目标时由服务端解析默认目录,bootstrap 的模块身份、参数、变量与配置卡来自同一目录。启用哪些模块由 `config.yml.enabled` 决定。
178
187
 
179
- > 根目录 [module.yml](module.yml) 覆盖 14 个公开共享参数与九层规则。`pnpm rebuild:preset-template` 从权威契约重建;规则默认关闭,共享参数按需取消注释。
188
+ > 根目录 [module.yml](module.yml) 覆盖 4 个公开共享参数与九层规则。`pnpm rebuild:preset-template` 从权威契约重建;规则默认关闭,共享参数按需取消注释。
180
189
 
181
190
  ## 提示词配置(九个官方插入点)
182
191
 
@@ -207,29 +216,29 @@ UI / 写盘按上表分组;这是展示顺序,不是模型提示词优先级
207
216
 
208
217
  工作台「模块」页按内容识别原生模块与 SillyTavern 来源;ST JSON/YAML 预设卡片经预览确认后,按注入层级映射为本地模块:
209
218
 
210
- - `prompts[]` → `promptConfigs`:system 角色进入 `system-section`,其余进入 `pre-step`;官方 `prompt_order[].order[]` 决定启停与相对顺序,深度位置保留来源并报告降级
219
+ - `prompts[]` → `rules[].do` 中的注入动作:system 角色进入 `system-section`,其余进入 `pre-step`;官方 `prompt_order[].order[]` 决定启停与相对顺序,深度位置保留来源并报告降级
211
220
  - 采样参数(`temperature` / `openai_max_tokens` / `reasoning_effort`)**剥离**——模型参数统一由「模型设置」UI / 宿主默认管理
212
221
  - ST 变量:保留可启停的赋值模板,在运行时顺序求值;声明变量在合并和角色卡应用时保持局部绑定
213
222
  - ST 管理工具:始终装配 `character-tools`、`session-var-tools` 与 `tool-config-engine`
214
- - `enable_web_search`:`true` → 额外组装 `tool-web`(fetch 启用);`false` → 产出三条 `triggers` 声明(`assembly` 呈现剔除 + `sdk-strip` 裁 `tools:sdk` 正文 + `guard` 执行层拒绝,共用同一份 `deny: [web_search, web_fetch]`),PTC 下同样生效
223
+ - `enable_web_search`:`true` → 额外组装 `tool-web`(fetch 启用);`false` → 产出三条规则(`assembly` 呈现剔除 + `sdk-strip` 裁 `tools:sdk` 正文 + `guard` 执行层拒绝,共用同一份 `deny: [web_search, web_fetch]`),PTC 下同样生效
215
224
  - 含有效 `character_book` 条目时自动追加 `world-book-tools` 模块,使导入预设可直接调用世界书管理工具
216
225
  - 世界书条目级条件:`delayUntilRecursion`(延迟到递归扫描的层级池)、`useGroupScoring`(组内评分淘汰)、`matchCreatorNotes` / `matchCharacterDepthPrompt`(按需扫描卡片备注与深度提示词)按 ST 语义求值;`characterFilter`(角色/标签过滤)、`automationId`(STscript 自动化)、`outletName` 与向量检索**不实现**,只保留来源事实并在预览卡里逐条告警
217
226
  - 触发键里的 ST 宏(例如只存在于 ST 全局 persona 的 `{{user}}`)登记为「模板变量」空占位并产出诊断:未赋值时该键不参与匹配(不会退化成字面量误判),在模板变量里赋值后按既有匹配路径生效
218
227
  - 角色卡 `extensions.depth_prompt` 保留为一条**默认禁用**的 pre-step 配置(ST 只在群聊自动注入),可在工作台手动启用
219
- - `modules` 按需装配:`prompt-config-engine` 与上述 ST 管理工具始终存在;含 system-section 时补 `persona`(`complete: false` 允许 system 段生效);含有效世界书条目时补 `world-book-tools`
228
+ - `modules` 按需装配:`rule-engine` 与上述 ST 管理工具承接转换产物;含 system-section 时补 `persona`(`complete: false` 允许 system 段生效);含有效世界书条目时补 `world-book-tools`
220
229
 
221
230
  转换结果是一个普通模块(id 由文件名生成),可在工作台「模块」页的模块列表中直接启用。字段级参数对照与完整示例见 [SillyTavern.md](docs/SillyTavern.md)。
222
231
 
223
- ### 角色卡(PNG / JSON)与角色卡库
232
+ ### 角色卡(PNG / JSON)导入
224
233
 
225
- 工作台「模块」页的角色卡素材区把角色卡导入**角色卡库**(`~/.dsh/.prompt-tool/.characters/<id>/`):
234
+ 工作台「模块」页将角色卡作为来源导入普通 `.prompt-tool/modules/<id>/`:
226
235
 
227
236
  - **PNG**:`ccv3` 优先 / `chara` 兜底;按魔数识别并受限解码,原始文件先暂存、预览确认后入库,保留原图。
228
237
  - **JSON/YAML**:支持 ST 角色数据和自包含原生角色片段。批次逐张预览,不把多份来源合并成一张角色卡;所有大小的文件都先预览再提交。
229
238
  - 正文映射:`first_mes` → 开场白(`dedupe: session`)、`alternate_greetings` → 备用开场白、
230
239
  `description/personality/scenario` → 角色设定;采样参数剥离(模型设置 UI 管理)
231
- - **导入到当前预设**:参数合并进当前预设 promptConfigs(`chara-<卡>-` 前缀、幂等);可一键移除
232
- - **角色记忆**:`memory.md` 跟随角色卡跨预设,应用时合并为 world-book constant 配置注入
240
+ - **统一模块身份**:人设、开场白和世界书写完整 module.yml,规则切片与其他模块一致;没有角色专属目录、前缀或登记
241
+ - **模块记忆**:memory.md 属于模块,更新保留本地记忆和用户资产;world_book_read_memory 按需读取,不自动注入
233
242
 
234
243
  ### 世界书(world-book 策略)
235
244
 
@@ -238,11 +247,11 @@ UI / 写盘按上表分组;这是展示顺序,不是模型提示词优先级
238
247
  - **ST 注入语义**:常驻候选或主键命中,再按副键逻辑、概率、分组与时序筛选;非常驻无主键不注入;原生手写 world-book 约定保持不变
239
248
  - **匹配选项**:`caseSensitive` / `wholeWords`;只有 `/pattern/flags` 形式识别为正则,其余为字面键
240
249
  - **管理**:模块列表顶部下拉选「世界书」过滤(完整模块卡片编辑 + 批量启用/禁用);
241
- 模型工具 `world_book_list/upsert/delete`(`note` 写入角色卡记忆)
250
+ 模型工具 world_book_list/upsert/delete(note 追加模块记忆)与 world_book_read_memory(按需读取)
242
251
  - **ST 变量**:赋值不在导入时执行;local/global 分表但只在会话内有效,嵌套宏有循环与大小保护。
243
252
  深度历史位置、system 角色、token 预算和 ST 扩展脚本不具备完整等价性,详见兼容边界
244
253
  - **会话变量**:`session_var` 工具(list/get/set/clear)维护角色状态(会话级覆盖预设默认,
245
- 结束即失);跨会话长期记忆用 `world_book` note(持久 memory.md 跟随角色卡)
254
+ 结束即失);跨会话文本通过 note 保存在模块 memory.md,读取失败会明确报错
246
255
 
247
256
  详细转换规则见 [SillyTavern.md](docs/SillyTavern.md)。
248
257
 
@@ -0,0 +1,49 @@
1
+ import { labelOf, requireString } from './shared.mjs'
2
+ import { createTurnStopBudget, pluginMessage } from '../layers.mjs'
3
+
4
+ export function prepareAppendContext(action, plugin) {
5
+ const label = labelOf(action)
6
+ const text = requireString(action.text, `${label}.text`, plugin)
7
+ if (text.length === 0) return
8
+ if (action.mode === 'continue') {
9
+ return (_ctx, { warnOnce, on, collect, take }) => {
10
+ const budget = createTurnStopBudget()
11
+ collect(on('agent/turn-stopping', ({ agent, turn } = {}) => {
12
+ try {
13
+ if (typeof action.match === 'function' && action.match(agent, turn) !== true) return
14
+ const session = agent?.session
15
+ if (session?.id === undefined || typeof agent.steer !== 'function') return
16
+ const entry = budget.entry(session.id, turn)
17
+ if (!budget.available(entry, turn) || !take({ agent })) return
18
+ // 计数在 steer 之前落账:steer 抛错也不允许下一步重试越过预算。
19
+ budget.claim(entry, turn)
20
+ // 身份归本动作:复用 pluginMessage 的形状,但 source.plugin 必须是动作命名空间
21
+ // (层模块的 pluginMessage 用 layers 自己的身份,那不是本动作的名字)。
22
+ const message = pluginMessage(`action-${label}`, text, `${label} continue`)
23
+ agent.steer({ ...message, source: { ...message.source, plugin: label } })
24
+ } catch (error) {
25
+ warnOnce(`${plugin}: append-context(continue) action ${label} failed: ${String(error?.message ?? error)}`)
26
+ }
27
+ }))
28
+ }
29
+ }
30
+ // 追加上下文:只回 user 角色的 durable 通知(宿主把 additionalContexts 落成 user 消息)。
31
+ return (_ctx, { warnOnce, on, collect, take }) => collect(on('tools/post-execute', async (exec, result, next) => {
32
+ const decision = await next()
33
+ try {
34
+ if (typeof action.match === 'function' && action.match(exec, result, decision) !== true) return decision
35
+ if (decision?.kind !== 'accept') return decision
36
+ // source 形状与 pluginMessage 同源:v4 要求 kind 是生产者名(`plugin:<name>`),
37
+ // 裸 `plugin` 会被会话格式校验拒绝,这条 durable user 通知就落不了盘。
38
+ const notice = {
39
+ ...pluginMessage(`action-${label}`, text, `${label} context`),
40
+ source: { kind: `plugin:${label}`, plugin: label, form: 'notice', summary: `${label} context` },
41
+ }
42
+ const updated = { ...decision, additionalContexts: [...(decision.additionalContexts ?? []), notice] }
43
+ return take(exec) ? updated : decision
44
+ } catch (error) {
45
+ warnOnce(`${plugin}: append-context action ${label} failed, keeping the plain result: ${String(error?.message ?? error)}`)
46
+ return decision
47
+ }
48
+ }))
49
+ }
@@ -0,0 +1,150 @@
1
+ import { NAME_LIST, createMask, labelOf, requireString } from './shared.mjs'
2
+ import { sessionEvents } from '../shared.mjs'
3
+
4
+ /**
5
+ * 动态白名单:从**本会话已持久化的** `tool/call` 事件里回收解锁名单,填入 `into`。
6
+ *
7
+ * 语义对齐原 `tool-bootstrap.mjs` 的 `unlockedFor(session)`:某个发现工具(如
8
+ * `dev_tool_search`)上一次调用时把要解锁的工具名写进了自己的调用参数,本函数把它读回来
9
+ * 填入 allow,于是**解锁能跨请求保留**——没有它,解锁是一次性的(当次请求用完即被裁掉)。
10
+ *
11
+ * 判据刻意保守,坏数据只影响该条、绝不上抛:
12
+ * - 只认 `event.type === 'tool/call'` 且 `event.data.name` **精确等于**声明里的 `tool`;
13
+ * - `arguments` 是模型产出的任意字符串,`JSON.parse` 必须 try/catch;解析出的必须是对象;
14
+ * - 只取 `key` 指向的**字符串数组**里的字符串项,非字符串项逐项丢弃,不展开对象、不 eval;
15
+ * - 任一步不成立就跳过该条事件,其余事件照常累积;全程无异常(读事件本身除外,由调用方兜)。
16
+ *
17
+ * `tool` / `key` **按参数传入**而不是向外层读取——这个模块会被多个 assembly 动作各挂一次,
18
+ * 任何模块级可变状态都会让它们互相覆盖。
19
+ */
20
+ function fillUnlocked(into, session, tool, key, warnOnce, plugin, label) {
21
+ into.clear()
22
+ let events
23
+ try {
24
+ events = sessionEvents(session)
25
+ } catch (error) {
26
+ warnOnce(`${plugin}: assembly action ${label}: reading session events for allowFrom failed: ${String(error?.message ?? error)}`)
27
+ return
28
+ }
29
+ for (const event of events) {
30
+ if (event?.type !== 'tool/call' || event?.data?.name !== tool) continue
31
+ const raw = event.data.arguments
32
+ const parsed = typeof raw === 'string' ? safeParse(raw) : raw
33
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) continue
34
+ const value = parsed[key]
35
+ if (!Array.isArray(value)) continue
36
+ for (const item of value) if (typeof item === 'string' && item.length > 0) into.add(item)
37
+ }
38
+ }
39
+
40
+ function safeParse(text) {
41
+ try {
42
+ return JSON.parse(text)
43
+ } catch {
44
+ return undefined
45
+ }
46
+ }
47
+
48
+ export function prepareAssembly(action, plugin) {
49
+ const target = action.target
50
+ if (target === null || typeof target !== 'object' || Array.isArray(target)) {
51
+ throw new TypeError(`${plugin}: assembly requires a target object — { tools } / { sections } / { contexts }`)
52
+ }
53
+ const toolsSource = target.tools
54
+ const allowFrom = toolsSource?.allowFrom
55
+ let allowExtra
56
+ if (allowFrom !== undefined) {
57
+ if (allowFrom === null || typeof allowFrom !== 'object' || Array.isArray(allowFrom)) {
58
+ throw new TypeError(`${plugin}: assembly.tools.allowFrom must be an object — { tool, key }`)
59
+ }
60
+ requireString(allowFrom.tool, 'assembly.tools.allowFrom.tool', plugin)
61
+ requireString(allowFrom.key, 'assembly.tools.allowFrom.key', plugin)
62
+ // deny 是黑名单、动态项只做加法,同声明的语义不可解释(且 allowFrom 已经覆盖不了 deny
63
+ // 的名字到底该不该出现),挂载期直接拒,而不是运行期猜。
64
+ if (Array.isArray(toolsSource.deny) && toolsSource.deny.length > 0) {
65
+ throw new TypeError(`${plugin}: assembly.tools cannot combine deny with allowFrom — 动态项只做加法,与黑名单语义冲突`)
66
+ }
67
+ allowExtra = new Set()
68
+ }
69
+ const tools = toolsSource === undefined ? undefined : createMask(toolsSource, 'assembly.tools', plugin, allowExtra)
70
+ const sectionsAdd = target.sections?.add
71
+ const sectionsRemove = NAME_LIST.parse(target.sections?.remove, plugin, 'assembly.sections.remove')
72
+ // 白名单(2026-09-22 用户拍板「扩动作库」):`personaSectionsOnly` 一类的语义是
73
+ // `filter(白名单)`,而 remove 是黑名单——未列名的第三方/晚到段不会被它删掉。
74
+ // 两种语义**互斥**:同时声明即挂载期 fail loud,而不是静默二选一。
75
+ const sectionsKeep = NAME_LIST.parse(target.sections?.keep, plugin, 'assembly.sections.keep')
76
+ if (sectionsKeep !== undefined && sectionsRemove !== undefined) {
77
+ throw new TypeError(`${plugin}: assembly target.sections cannot combine keep with remove — 一种是白名单、一种是黑名单,同时声明语义不明`)
78
+ }
79
+ const contextsAdd = target.contexts?.add
80
+ const contextsRemove = NAME_LIST.parse(target.contexts?.remove, plugin, 'assembly.contexts.remove')
81
+ const contextsClear = target.contexts?.clear === true
82
+ for (const [key, list] of [['sections.add', sectionsAdd], ['contexts.add', contextsAdd]]) {
83
+ if (list === undefined) continue
84
+ if (!Array.isArray(list)) throw new TypeError(`${plugin}: assembly.${key} must be an array of { name, text }`)
85
+ for (const entry of list) {
86
+ if (entry === null || typeof entry !== 'object') throw new TypeError(`${plugin}: assembly.${key} entries must be objects`)
87
+ requireString(entry.name, `assembly.${key}[].name`, plugin)
88
+ requireString(entry.text, `assembly.${key}[].text`, plugin)
89
+ }
90
+ }
91
+ return (_ctx, { warnOnce, on, collect, take }) => collect(on('system-prompt/assemble', async (assembly, context, next) => {
92
+ // Downstream errors propagate untouched; only this action's own logic is guarded.
93
+ const assembled = await next()
94
+ try {
95
+ if (typeof action.match === 'function' && action.match(context, assembled) !== true) return assembled
96
+ let result = assembled
97
+ if (tools !== undefined && Array.isArray(result.tools)) {
98
+ // 每轮重算解锁名单:本轮装配要反映**本会话至今**的全部解锁调用。
99
+ if (allowExtra !== undefined) {
100
+ fillUnlocked(allowExtra, context?.session ?? context?.agent?.session, allowFrom.tool, allowFrom.key, warnOnce, plugin, labelOf(action))
101
+ }
102
+ // keep 名单兜底(2026-09-22 用户拍板,**可选**):判据与原 `tool-bootstrap` 逐条对齐——
103
+ // 原模块是 `missing = required.filter(t => !available.has(t))`
104
+ // 且 `missing.length > 0` 即放弃裁剪,也就是「**任意一个** 名单工具缺失」而不是
105
+ // 「一个都不存在」(对拍实测:目录 `[bash,read]` + 名单 `[bash,str_replace_editor]` 时
106
+ // 原模块给完整目录,只判"全缺"会裁成 `[bash]`)。
107
+ // 声明写 `requireMatch: true` 才启用;**不写就照名单裁**(与 `tool-filter.mjs` 一致)。
108
+ const available = new Set(result.tools.map((tool) => tool?.name).filter((name) => typeof name === 'string'))
109
+ // 必需名单 = 静态 allow ∪ 本轮解锁项:解锁项同样是「本该存在」的工具,缺了就 fail-open。
110
+ const required = tools.allow === undefined && allowExtra === undefined
111
+ ? undefined
112
+ : new Set([...(tools.allow ?? []), ...(allowExtra ?? [])])
113
+ const missingRequired = tools.requireMatch && required !== undefined
114
+ && [...required].some((name) => !available.has(name))
115
+ if (missingRequired) {
116
+ warnOnce(`${plugin}: assembly action ${labelOf(action)}: a requireMatch tool is absent — exposing the full tool set (fail-open)`)
117
+ } else {
118
+ const kept = result.tools.filter((tool) => !tools.blocks(tool?.name))
119
+ if (kept.length !== result.tools.length) result = { ...result, tools: kept }
120
+ }
121
+ }
122
+ if (Array.isArray(result.sections) && (sectionsAdd !== undefined || sectionsRemove !== undefined || sectionsKeep !== undefined)) {
123
+ const kept = sectionsKeep !== undefined
124
+ ? result.sections.filter((section) => sectionsKeep.has(section?.name))
125
+ : sectionsRemove === undefined
126
+ ? result.sections
127
+ : result.sections.filter((section) => !sectionsRemove.has(section?.name))
128
+ if (kept.length !== result.sections.length || sectionsAdd?.length > 0) {
129
+ result = { ...result, sections: sectionsAdd === undefined ? kept : [...kept, ...sectionsAdd] }
130
+ }
131
+ }
132
+ if (Array.isArray(result.contexts)) {
133
+ if (contextsClear) {
134
+ if (result.contexts.length > 0) result = { ...result, contexts: [] }
135
+ } else if (contextsAdd !== undefined || contextsRemove !== undefined) {
136
+ const kept = contextsRemove === undefined
137
+ ? result.contexts
138
+ : result.contexts.filter((entry) => !contextsRemove.has(entry?.name))
139
+ if (kept.length !== result.contexts.length || contextsAdd?.length > 0) {
140
+ result = { ...result, contexts: contextsAdd === undefined ? kept : [...kept, ...contextsAdd] }
141
+ }
142
+ }
143
+ }
144
+ return result !== assembled && take(context) ? result : assembled
145
+ } catch (error) {
146
+ warnOnce(`${plugin}: assembly action ${labelOf(action)} failed, exposing the full assembly: ${String(error?.message ?? error)}`)
147
+ return assembled
148
+ }
149
+ }))
150
+ }
@@ -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
+ })