dsh-plugin-prompt-tool 3.0.1 → 3.1.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 (57) hide show
  1. package/README.md +17 -1
  2. package/engine/actions/catalog.mjs +2 -1
  3. package/engine/actions/index.mjs +5 -3
  4. package/engine/actions/shared.mjs +26 -12
  5. package/engine/actions/tool-visibility.mjs +31 -14
  6. package/engine/compositions/source/local/context-provenance.yml +9 -0
  7. package/engine/compositions/source/local/generation-recovery.yml +10 -0
  8. package/engine/compositions/source/local/retrieval.yml +7 -0
  9. package/engine/compositions/source/local/soul-ledger.yml +3 -0
  10. package/engine/compositions/source/local/soul-maintenance.yml +9 -0
  11. package/engine/conditions/examples.mjs +1 -0
  12. package/engine/conditions/index.mjs +7 -0
  13. package/engine/conditions/manifest.mjs +28 -0
  14. package/engine/context-idle-compaction.mjs +58 -0
  15. package/engine/context-provenance.d.mts +7 -0
  16. package/engine/context-provenance.mjs +156 -0
  17. package/engine/context-pruner.mjs +64 -0
  18. package/engine/context-routing.mjs +45 -0
  19. package/engine/context-summary.mjs +80 -0
  20. package/engine/fillers.mjs +37 -4
  21. package/engine/generation-recovery-coordinator.mjs +40 -0
  22. package/engine/generation-recovery-maintenance.mjs +18 -0
  23. package/engine/generation-recovery.mjs +153 -0
  24. package/engine/host-package.mjs +13 -0
  25. package/engine/layers.d.mts +4 -0
  26. package/engine/layers.mjs +57 -20
  27. package/engine/present-as.mjs +2 -2
  28. package/engine/retrieval-reference.d.mts +5 -0
  29. package/engine/retrieval-reference.mjs +26 -0
  30. package/engine/retrieval.mjs +17 -0
  31. package/engine/rule-engine.mjs +1 -0
  32. package/engine/rule-runtime.d.mts +8 -1
  33. package/engine/rule-runtime.mjs +18 -10
  34. package/engine/rule-spec.mjs +4 -2
  35. package/engine/schema.mjs +1 -1
  36. package/engine/service-names.d.mts +5 -0
  37. package/engine/service-names.mjs +5 -0
  38. package/engine/soul-ledger.mjs +8 -0
  39. package/engine/soul-maintenance.mjs +20 -0
  40. package/lib/client.js +16 -6
  41. package/lib/client.js.map +1 -1
  42. package/lib/index.mjs +1953 -246
  43. package/modules/soul/assets/ledger.md +4 -2
  44. package/modules/soul/assets/manifest.yml +2 -0
  45. package/modules/soul/assets/reference/00-layers.md +27 -27
  46. package/modules/soul/assets/reference/02-echo.md +161 -161
  47. package/modules/soul/assets/reference/05-doctor.md +67 -67
  48. package/modules/soul/assets/reference/10-echo-anatomy.md +109 -109
  49. package/modules/soul/assets/reference/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -202
  50. package/modules/soul/assets/reference/ECHO/345/205/255/345/261/202/344/270/216mnemon/345/215/217/345/220/214/350/257/264/346/230/216/344/271/246.md +229 -229
  51. package/modules/soul/assets/templates/agents-template.md +3 -0
  52. package/modules/soul/assets/templates/self-template.md +19 -0
  53. package/modules/soul/module.yml +71 -13
  54. package/modules/tool-surface/module.yml +8 -5
  55. package/package.json +1 -1
  56. package/skills/dsh-module/reference.md +2 -2
  57. package/templates/80-tool-surface.yml +8 -7
package/README.md CHANGED
@@ -67,6 +67,8 @@ rules:
67
67
 
68
68
  规则是唯一行为源;共享模型与能力参数保存于 layerSettings,人设、变量与自定义工具各有独立顶层段。根 [module.yml](module.yml) 从权威目录生成九层及27个共享参数参考,示例默认关闭。
69
69
 
70
+ 单个模块装配失败时,首次加载跳过它,更新时恢复它的上次有效贡献;其他正常模块继续加载和更新。晚到服务的初始化失败也按此处理,等待中的新版本不会覆盖最后成功版本。故障仍会报告,修正该模块定义或显式刷新后重试。全局配置和跨模块独占冲突仍明确拒绝,不自动选择赢家;完整边界见[运行时装配](docs/engine-reuse.md)。
71
+
70
72
  - 条件使用 if,动作用 then,另有 else;动作身份在完整规则树中唯一。
71
73
  - 注入正文策略为 static、placeholder、context-activation;文本与长度分类、确认、工具名和受众由通用条件承担,正文、投递、请求与流控制由动作组合。
72
74
  - 工具提示通过 tool-catalog 只读引用同规则工具可见面声明,自动列出常驻名单与发现工具;提示正文和投递由模块规则控制。
@@ -110,6 +112,8 @@ persona:
110
112
  | module-file-tools | module_file_read / module_file_append |
111
113
  | module-manifest-tools | module_manifest |
112
114
  | module-import-tools | module_import |
115
+ | soul-ledger | soul_ledger |
116
+ | retrieval | module_search / module_open |
113
117
  | tool-config-engine | customTools 中的实际工具 |
114
118
 
115
119
  完整规则工具先读原始树与版本,再提交合法完整候选;删除整规则需显式 rule:null。变量工具提供 global/module 的 list/get/set/delete,set 必须显式字符串。模块私有文件(memory.md、assets/ 只读、data/ 只追加)由 module_file_read/append 读写,不自动注入;module_manifest 按 assets/manifest.yml 做 next/check/apply,只写 data/、不覆盖。
@@ -124,7 +128,19 @@ ST JSON 预设、PNG/JSON 角色卡和独立世界书,经统一预览/确认/
124
128
 
125
129
  outlet/vectorized/triggers 不能等价执行,相关规则禁用并报告;角色过滤、自动化、深度插入、预算、群聊和脚本都有明确限制。完整映射及报告边界见 [SillyTavern 导入](docs/SillyTavern.md)。
126
130
 
127
- 模块导出分享完整定义和允许的附件,不分享全局变量或私有 memory.md。旧私有记忆标记必须显式处理,不靠删除标记继续导出。路径、链接、体积、覆盖和备份边界见 [资产交换](docs/asset-transfer.md)。
131
+ 模块导出分享完整定义和允许的附件,不分享全局变量、私有 memory.md 或 data/。旧私有记忆标记必须显式处理,不靠删除标记继续导出。路径、链接、体积、覆盖和备份边界见 [资产交换](docs/asset-transfer.md)。
132
+
133
+ 书房数据通过 `/prompt-tool data-import <JSON>` 显式导入。先指定 `moduleId`、来源书房根 `sourceDirectory` 和相对文件列表 `paths`,预览创建、跳过与冲突项;确认后在相同 JSON 中加上返回的 `expectedRevision`(值取预览的 `revision`)应用。只补缺失文件,冲突项保留目标,源文件不动;旧 `02-墨.jsonl` 映射到 `data/02-soul.jsonl`。新建书房用 `{"moduleId":"soul","initialize":true}` 预览模块自己的清单,也须带预览版本应用。完整例子与限制见 [数据导入](docs/architecture-params.md#书房数据预览与导入)。
134
+
135
+ Soul 模块包含结构化账本、授权文件/历史检索、每步动态 SELF、上下文压缩与裁剪溯源、生成恢复和维护心跳。SELF 由 `rule-engine` 的内置动态内容源提供(`strategy: placeholder`、`fill: soul-self`),无需模块附带策略 JS。先用上述初始化预览补齐 `SELF.src.md` 骨架;已有模块副本须显式更新,包升级不会覆盖用户定义或私有数据,旧副本仍可使用自身保留的自定义策略。
136
+
137
+ `soul_ledger` 自动署名真实会话,`lead` 需宿主审批,只有最后主导会话可追加 `promise`;通用文件追加拒绝新旧账本名。`module_search` 返回的引用交给 `module_open` 回取,历史会话只读取声明的白名单。SELF 的当前身份在每步读取时加入,维护默认每分钟只更新索引与快照,`wake` 独立且默认关闭。
138
+
139
+ `generation-recovery` 的通用默认只截断;Soul 显式启用空回合检测和有界恢复,主会话额度 3、子代理 1,每会话独立且重挂不清零。维护压缩和扩大范围默认关闭。配置与边界见 [引擎运行时](docs/engine-reuse.md#重复输出安全截断)、[账本与SELF](docs/architecture-params.md#soul账本与派生快照)。
140
+
141
+ 这些运行能力由本插件实现,不需要源项目 `dsh-agent-soul` 的仓库、插件或后台服务;上下文治理仍要求宿主提供[官方能力包](docs/engine-reuse.md#上下文运行时与溯源)(按包名白名单从 DSH 自带包解析,运行期不比对版本)。本模块不等价于源项目的全部功能:检索采用授权范围内的字面匹配,未提供源近义检索、上下文水位提醒、完整 doctor/诊断工具或附属面板;恢复判据与额度策略也有差异。`assets/reference/` 保留的源文档用于背景参考,其中的源 CLI 和第三方工具不表示本插件已提供对应能力。
142
+
143
+ 上下文能力按包名白名单从 DSH 自带包解析,不增加插件依赖,运行期不校验版本;仅包缺失或未登记时拒绝该能力装配。包代码更新需要用户重启 DSH 加载;加载后修改模块启停/参数沿运行时刷新,不改官方 preset 或 profile patch。
128
144
 
129
145
  ## 升级与旧格式
130
146
 
@@ -74,7 +74,8 @@ export const ACTION_KINDS = Object.freeze({
74
74
  note: '一份声明拥有工具的可见面:allow/deny 名单、requireMatch、发现工具。名单语义对齐官方 '
75
75
  + 'ToolRestriction 的 and/or(同时给时先按 allow 保留、再按 deny 剔除)。deny 走 guard(本层与晚到工具)'
76
76
  + '与 restrict(继承的全局工具);allow 只有 restrict 能覆盖全局工具,本层注册无法在执行层「只保留」——'
77
- + '这是官方 restrict 的边界,不是本动作的取舍。',
77
+ + '这是官方 restrict 的边界,不是本动作的取舍。requireMatch 表达「名单与装配面整体错位」:'
78
+ + '名单里的名字一个都不存在才整面放宽,部分缺失只留诊断;解锁名不参与判定。',
78
79
  },
79
80
  'request-params': {
80
81
  title: '改模型请求参数',
@@ -83,9 +83,7 @@ export function prepareAction(action, options = {}) {
83
83
  const on = withWhen(options.on ?? channelBinder(ctx, kind, plugin, options.prepend === true), options.when, warnOnce, phase, missAll)
84
84
  // 每轮预算的轮边界由 durable 事件驱动(与谓词的 observe 同一事件源,但**独立记账**:
85
85
  // 预算是「本动作生效次数」,不是 durable 事件计数)。
86
- if (budget !== undefined) collect(ctx.on('session/event', (session, event) => budget.observe(session, event)))
87
86
  const take = (subject) => budget?.take(subject) ?? true
88
- bind?.(ctx, { warnOnce, on, collect, take, onMiss, agent: options.agent })
89
87
  const dispose = () => {
90
88
  for (const release of disposers.splice(0)) {
91
89
  try {
@@ -95,7 +93,11 @@ export function prepareAction(action, options = {}) {
95
93
  }
96
94
  }
97
95
  }
98
- keepDisposer(ctx, dispose, `${plugin}: action ${labelOf(action)}`)
96
+ try {
97
+ if (budget !== undefined) collect(ctx.on('session/event', (session, event) => budget.observe(session, event)))
98
+ bind?.(ctx, { warnOnce, on, collect, take, onMiss, agent: options.agent })
99
+ keepDisposer(ctx, dispose, `${plugin}: action ${labelOf(action)}`)
100
+ } catch (error) { dispose(); throw error }
99
101
  // 调用方(rule-runtime)把 miss 挂到该动作的条目上;没有回调时保持 undefined。
100
102
  dispose.miss = missHandlers.length === 0 ? undefined : missAll
101
103
  return dispose
@@ -37,20 +37,28 @@ export function createMask(source, label, plugin, allowExtra) {
37
37
  /** 动态白名单的落点(活集合,由调用方每轮重填);未声明 allowFrom 时不存在。 */
38
38
  allowExtra,
39
39
  /**
40
- * fail-open 兜底开关(**可选**,缺省 false):原 `tool-bootstrap` 的语义是
41
- * 「keep 名单里的工具在本次装配目录里一个都不存在 → 放弃裁剪、暴露完整目录」,而原
42
- * `tool-filter` 的同名场景是**照名单裁成空目录**。两种语义都真实存在,所以这里
43
- * **不设默认**——要兜底的声明自己写 `requireMatch: true`。把某一个模块的特有语义当成
44
- * 通用默认,会让另一个声明路径的行为被悄悄改掉(这正是对拍抓出来的)。
40
+ * fail-open 兜底开关(**可选**,缺省 false):判据是「这份名单与装配面整体错位」——
41
+ * 名单里的名字**一个都不存在**时放弃裁剪、暴露完整目录;部分缺失只留诊断,收窄照常
42
+ * 生效(提供方被替换或关闭是正常部署差异,见 {@link toolSurfacePlan})。原
43
+ * `tool-bootstrap` 的同名语义就是「一个都不存在 → 放弃裁剪」,而原 `tool-filter` 是
44
+ * 「永远照名单裁」;两种语义都真实存在,所以这里**不设默认**——要兜底的声明自己写
45
+ * `requireMatch: true`。把某一个模块的特有语义当成通用默认,会让另一个声明路径的行为
46
+ * 被悄悄改掉(这正是对拍抓出来的)。
45
47
  */
46
48
  requireMatch: source?.requireMatch === true,
47
- /** 与 tool-filter 的名单判据 / Layer.admits 同一判据(tool-filter 模块已删除,判据保留)。 */
48
- blocks: (toolName) => {
49
+ /**
50
+ * 与 tool-filter 的名单判据 / Layer.admits 同一判据(tool-filter 模块已删除,判据保留)。
51
+ *
52
+ * 第二个参数是**解锁项快照**:allowExtra 是跨 agent 共享的活集合,每次装配都会 clear 后
53
+ * 按当前会话重填;执行闸在该装配之后才被调用,必须读自己会话的冻结快照,否则别的会话
54
+ * (或探针装配)会把本会话的解锁名抹掉。缺省仍是活集合,供同一次装配内的裁剪使用。
55
+ */
56
+ blocks: (toolName, extra = allowExtra) => {
49
57
  if (typeof toolName !== 'string' || toolName.length === 0) return false
50
58
  // 保留传输不参与任何名单:allow 没点名也必须留下,deny 已在挂载期拒绝。
51
59
  if (toolName === RUN_CODE) return false
52
60
  if (deny !== undefined && deny.has(toolName)) return true
53
- if (allowExtra?.has(toolName) === true) return false
61
+ if (extra?.has(toolName) === true) return false
54
62
  // 声明了 allow、或声明了 allowFrom(= 白名单模式,静态名单视为空集),未点名即裁掉。
55
63
  return (allow !== undefined || allowExtra !== undefined) && allow?.has(toolName) !== true
56
64
  },
@@ -60,17 +68,23 @@ export function createMask(source, label, plugin, allowExtra) {
60
68
  /**
61
69
  * 工具可见面的单一判据:装配目录、`tools:sdk` 正文与执行闸共用同一份 fail-open 决定。
62
70
  *
63
- * `available` = 目录名 ∪ SDK 正文名;`required` = 静态 allow ∪ 动态解锁项,再剔掉
71
+ * `available` = 目录名 ∪ SDK 正文名;`required` 只取**静态 allow**,再剔掉
64
72
  * `run_code`(保留传输)与 `mask.blocks()` 为真的名字(deny 覆盖者不该被要求存在)。
65
- * 缺项时三面必须一起放宽——只放宽一面会让模型看到的与能调的不一致。
73
+ *
74
+ * 放宽条件是「名单里的名字**一个都不存在**」:那才说明装配面与声明整体错位(换了一套
75
+ * 部署/平台)。只缺一部分是提供方被替换或关掉的正常部署差异——照常收窄,缺的工具由发现
76
+ * 通道补位,替换提供方的名字由部署层名单登记。解锁名(allowExtra)**不参与判定**:它是只做
77
+ * 加法的提示,若参与,模型解锁一个拼错或已改名的工具就能关掉收窄。
78
+ *
79
+ * 放宽时三面必须一起放宽——只放宽一面会让模型看到的与能调的不一致。
66
80
  */
67
81
  export function toolSurfacePlan(mask, available) {
68
82
  // 动作层的挂载期校验(prepareToolVisibility)已拒绝 requireMatch + 无 allow。
69
83
  const required = mask.requireMatch
70
- ? [...new Set([...(mask.allow ?? []), ...(mask.allowExtra ?? [])])].filter((name) => name !== RUN_CODE && !mask.blocks(name))
84
+ ? [...new Set(mask.allow ?? [])].filter((name) => name !== RUN_CODE && !mask.blocks(name))
71
85
  : []
72
86
  const missing = required.filter((name) => !available.has(name))
73
- return { failOpen: missing.length > 0, missing }
87
+ return { failOpen: required.length > 0 && missing.length === required.length, missing }
74
88
  }
75
89
 
76
90
  export function requireString(value, label, plugin) {
@@ -13,8 +13,10 @@
13
13
  *
14
14
  * 名单语义对齐官方 ToolRestriction(deepseek-harness packages/core/tools/src/index.ts:701-706):
15
15
  * allow 与 deny 可以同时给,判据是「先按 allow 保留,再按 deny 剔除」。requireMatch 只在
16
- * allow 模式有意义:名单里任一工具在目录与 SDK 正文的并集里都找不到,就放弃裁剪、暴露完整目录(fail-open)并告警——
17
- * 收窄错了会让当前工作流调不到工具,比多花上下文严重。
16
+ * allow 模式有意义,判据是「这份名单与装配面整体错位」:名单里的名字**一个都不在**目录与
17
+ * SDK 正文的并集里,才放弃裁剪、暴露完整目录(fail-open)并告警。只缺一部分是提供方被
18
+ * 替换或关闭的正常部署差异,收窄照常生效、缺项只留一条诊断;解锁名是只做加法的提示,
19
+ * 不参与这个判定。
18
20
  *
19
21
  * 执行边界:deny 同时用 restrict(继承面)与 guard(本层/晚到);allow 不用 restrict——
20
22
  * 它的 allow 只能收继承面名字,而 `schemas()` 全局视图会漏掉预设/祖先层,局部 allow 会
@@ -110,7 +112,8 @@ export function prepareToolVisibility(action, plugin) {
110
112
  const capability = createAgentCapability((agent) => {
111
113
  const tools = agent?.ctx?.tools
112
114
  if (tools === undefined) return undefined
113
- const state = { failOpen: false }
115
+ // unlocked:本会话装配时冻结的解锁名快照(await next() 期间别的装配会重填共享集合)。
116
+ const state = { failOpen: false, unlocked: undefined }
114
117
  const disposers = []
115
118
  try {
116
119
  const known = toolScopeSnapshot(tools).globalNames
@@ -135,8 +138,11 @@ export function prepareToolVisibility(action, plugin) {
135
138
  const toolName = exec?.name
136
139
  if (toolName === RUN_CODE || typeof toolName !== 'string' || toolName.length === 0) return undefined
137
140
  if (deny?.has(toolName)) return `${label}: tool ${JSON.stringify(toolName)} is denied by tool-visibility`
138
- if (mask.allow !== undefined && !state.failOpen && mask.blocks(toolName)) {
139
- return `${label}: tool ${JSON.stringify(toolName)} is not allowed by tool-visibility`
141
+ if (mask.allow !== undefined && !state.failOpen && mask.blocks(toolName, state.unlocked)) {
142
+ const hint = discovery === undefined
143
+ ? 'the allow list is the only path; this action has no discovery source'
144
+ : `unlock it with tool_search({"toolNames":[${JSON.stringify(toolName)}]}); the gate allows unlocked tools from the next request on`
145
+ return `${label}: tool ${JSON.stringify(toolName)} is not allowed by tool-visibility; ${hint}`
140
146
  }
141
147
  return undefined
142
148
  }))
@@ -148,8 +154,9 @@ export function prepareToolVisibility(action, plugin) {
148
154
  return { disposers, data: state }
149
155
  })
150
156
 
151
- const syncGate = (agent) => {
152
- const key = `${maskKey(mask)}|unlock:${allowExtra === undefined ? '' : [...allowExtra].sort().join(',')}`
157
+ // 重挂判定用**本会话的快照**:共享集合在别的装配里会被重填,不能当身份。
158
+ const syncGate = (agent, unlocked) => {
159
+ const key = `${maskKey(mask)}|unlock:${unlocked === undefined ? '' : [...unlocked].sort().join(',')}`
153
160
  capability.sync(agent, key)
154
161
  }
155
162
 
@@ -183,8 +190,11 @@ export function prepareToolVisibility(action, plugin) {
183
190
  fillUnlocked(allowExtra, context?.session ?? context?.agent?.session, unlock.tool, unlock.key, warnOnce, plugin, label)
184
191
  }
185
192
  }
193
+ // 在 await next() 之前冻结本会话的解锁名:之后的裁剪与执行闸都读这一份,
194
+ // 共享的 allowExtra 可能被并发的另一次装配(别的会话 / 探针)清空重填。
195
+ const unlocked = allowExtra === undefined ? undefined : new Set(allowExtra)
186
196
  try {
187
- syncGate(context?.agent)
197
+ syncGate(context?.agent, unlocked)
188
198
  } catch (error) {
189
199
  // 闸注册失败必须留痕;呈现面照常结算,绝不把异常抛给装配。
190
200
  warnOnce(`${plugin}: tool-visibility action ${label} failed to sync execution gate: ${String(error?.message ?? error)}`)
@@ -204,16 +214,23 @@ export function prepareToolVisibility(action, plugin) {
204
214
  // 没有工具面可观(既无目录也无 SDK 正文)时不裁剪、不告警、不动执行闸:
205
215
  // 空 surface 不等于「名单全缺」;空目录(tools=[])仍是真实观测,照旧 fail-open。
206
216
  if (!Array.isArray(result.tools) && sdkSection === undefined) return result
207
- // 三面(目录 / 正文 / 执行闸)共用这一份判断:缺项就整体放宽,呈现与执行不得两套口径。
217
+ // 三面(目录 / 正文 / 执行闸)共用这一份判断:名单全缺才整体放宽,呈现与执行不得两套口径。
208
218
  const plan = toolSurfacePlan(mask, available)
209
219
  const state = capability.get(context?.session ?? context?.agent?.session)
210
- if (state !== undefined) state.data.failOpen = plan.failOpen
220
+ if (state !== undefined) {
221
+ state.data.failOpen = plan.failOpen
222
+ state.data.unlocked = unlocked
223
+ }
211
224
  if (plan.failOpen) {
212
- // 去重键固定到动作:缺项名随解锁历史变化,不能让每个新集合各占一条 warnOnce。
213
- warnOnce(`${plugin}: tool-visibility action ${label}: requireMatch tools absent (${plan.missing.join(', ')}) — exposing the full tool set (fail-open)`, `${plugin}: tool-visibility action ${label}: requireMatch fail-open`)
225
+ // 去重键固定到动作:缺项名随部署变化,不能让每个新集合各占一条 warnOnce。
226
+ warnOnce(`${plugin}: tool-visibility action ${label}: none of the resident tools exist (${plan.missing.join(', ')}) — exposing the full tool set (fail-open)`, `${plugin}: tool-visibility action ${label}: requireMatch fail-open`)
214
227
  } else {
228
+ // 部分缺失不改变裁剪,但必须留痕:否则「某个常驻工具为什么不见了」只能靠猜。
229
+ if (plan.missing.length > 0) {
230
+ warnOnce(`${plugin}: tool-visibility action ${label}: resident tools absent (${plan.missing.join(', ')}) — narrowing continues; register replacement providers in the deployment allow list`, `${plugin}: tool-visibility action ${label}: resident tools absent`)
231
+ }
215
232
  if (Array.isArray(result.tools)) {
216
- const kept = result.tools.filter((tool) => !mask.blocks(tool?.name))
233
+ const kept = result.tools.filter((tool) => !mask.blocks(tool?.name, unlocked))
217
234
  if (kept.length !== result.tools.length) result = { ...result, tools: kept }
218
235
  }
219
236
  if (Array.isArray(result.sections)) {
@@ -227,7 +244,7 @@ export function prepareToolVisibility(action, plugin) {
227
244
  }
228
245
  const names = mask.allow === undefined
229
246
  ? mask.deny
230
- : new Set([...presentNames()].filter((name) => mask.blocks(name)))
247
+ : new Set([...presentNames()].filter((name) => mask.blocks(name, unlocked)))
231
248
  if (names === undefined || names.size === 0) return section
232
249
  const stripped = stripSdkDeclarations(section.text, names)
233
250
  if (stripped === section.text) return section
@@ -0,0 +1,9 @@
1
+ - id: context-provenance
2
+ name: ./engine/context-provenance.mjs
3
+ config:
4
+ enabled: true
5
+ audience: all
6
+ auto: true
7
+ modelSwitch: true
8
+ prune: true
9
+ recoveryRetainTokens: 1024
@@ -0,0 +1,10 @@
1
+ # module: generation-recovery
2
+ # 精确重复安全截断;不自动续跑,不处理压缩或标题等辅助模型调用。
3
+ - id: generation-recovery
4
+ name: ./engine/generation-recovery.mjs
5
+ config:
6
+ enabled: true
7
+ audience: all
8
+ textRepeatChars: 4096
9
+ reasoningRepeatChars: 8192
10
+ maxCycleChars: 256
@@ -0,0 +1,7 @@
1
+ # module: retrieval
2
+ - id: retrieval
3
+ name: ./engine/retrieval.mjs
4
+ config:
5
+ enabled: true
6
+ paths: []
7
+ sessions: []
@@ -0,0 +1,3 @@
1
+ # module: soul-ledger
2
+ - id: soul-ledger
3
+ name: ./engine/soul-ledger.mjs
@@ -0,0 +1,9 @@
1
+ # module: soul-maintenance
2
+ - id: soul-maintenance
3
+ name: ./engine/soul-maintenance.mjs
4
+ config:
5
+ enabled: true
6
+ intervalMs: 60000
7
+ wake: false
8
+ audience: all
9
+ budgetBytes: 12288
@@ -9,4 +9,5 @@ export const PREDICATE_EXAMPLES = {
9
9
  scope: { audience: 'main', modelScope: 'all' },
10
10
  anchor: { subject: 'assistantReasoning', keys: ['确认'], mode: 'prefix' },
11
11
  variable: { key: 'quota', op: 'lte', value: '0', scope: 'effective' },
12
+ manifest: { ok: true },
12
13
  }
@@ -8,6 +8,7 @@ import { createPresetPredicate } from './preset.mjs'
8
8
  import { createScopePredicate } from './scope.mjs'
9
9
  import { createAnchorPredicate } from './anchor.mjs'
10
10
  import { createVariablePredicate } from './variable.mjs'
11
+ import { createManifestPredicate } from './manifest.mjs'
11
12
  import { withAvailableInput } from './availability.mjs'
12
13
  import { composite } from './composite.mjs'
13
14
  import { COMPOSITE_OPERATORS } from './operators.mjs'
@@ -21,6 +22,7 @@ export * from './preset.mjs'
21
22
  export * from './scope.mjs'
22
23
  export * from './anchor.mjs'
23
24
  export * from './variable.mjs'
25
+ export * from './manifest.mjs'
24
26
  export { subjectOf, channelTextSubjects, channelFactSubjects, FACT_PREDICATE_SUBJECTS } from './subject.mjs'
25
27
  export { composite } from './composite.mjs'
26
28
  export const PREDICATE_FACTORIES = Object.freeze({
@@ -34,6 +36,7 @@ export const PREDICATE_FACTORIES = Object.freeze({
34
36
  scope: createScopePredicate,
35
37
  anchor: createAnchorPredicate,
36
38
  variable: createVariablePredicate,
39
+ manifest: createManifestPredicate,
37
40
  })
38
41
  export { COMPOSITE_OPERATORS }
39
42
 
@@ -95,5 +98,9 @@ export function compileWhen(node, context = {}) {
95
98
  if (key === 'variable') {
96
99
  return createVariablePredicate(options, { store: context.variableStore, moduleId: context.moduleId })
97
100
  }
101
+ // manifest 谓词同样自带三值:读取器未绑定或清单不可读时为 UNAVAILABLE。
102
+ if (key === 'manifest') {
103
+ return createManifestPredicate(options, { manifestCheck: context.manifestCheck, moduleId: context.moduleId })
104
+ }
98
105
  return withAvailableInput(key, options, factory(options))
99
106
  }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * manifest 谓词:规则读取来源模块 `assets/manifest.yml` 的 checks 事实。
3
+ *
4
+ * 读取器由宿主装配经 options 在挂载期绑定,结果缓存 1000ms,清单戳变化提前失效;
5
+ * 读取器缺失、清单缺失/损坏都判 UNAVAILABLE——不能经 not 反转,也不能当「检查失败」。
6
+ */
7
+ import { UNAVAILABLE } from './availability.mjs'
8
+
9
+ /**
10
+ * @param {{ ok?: boolean }} options
11
+ * @param {{ manifestCheck?: { read: (moduleId: string) => boolean | undefined }, moduleId?: string }} [context]
12
+ */
13
+ export function createManifestPredicate(options = {}, context = {}) {
14
+ const unknown = Object.keys(options).filter((key) => key !== 'ok')
15
+ if (unknown.length > 0) throw new TypeError(`predicates: manifest does not take ${unknown.join(', ')}`)
16
+ if (typeof options.ok !== 'boolean') throw new TypeError('predicates: manifest ok must be a boolean')
17
+ const expected = options.ok
18
+ const reader = context.manifestCheck
19
+ const moduleId = context.moduleId
20
+ const predicate = () => {
21
+ if (reader === undefined || typeof reader.read !== 'function') return UNAVAILABLE
22
+ if (typeof moduleId !== 'string' || moduleId.length === 0) return UNAVAILABLE
23
+ const observed = reader.read(moduleId)
24
+ return typeof observed === 'boolean' ? observed === expected : UNAVAILABLE
25
+ }
26
+ predicate.kind = 'manifest'
27
+ return predicate
28
+ }
@@ -0,0 +1,58 @@
1
+ import { randomUUID } from 'node:crypto'
2
+ import { isDeepStrictEqual } from 'node:util'
3
+ import { importHostCapability } from './host-package.mjs'
4
+ import { frameSummary } from './context-summary.mjs'
5
+ import { historyEvents } from './history.mjs'
6
+
7
+ /** 已领取idle维护窗口内的局部事务;Basic.compactRegion仅允许打开的turn。 */
8
+ export async function compactIdleRegion({ agent, range, signal, meter, sessions, compaction, pairing }) {
9
+ signal.throwIfAborted()
10
+ const { createUserMessage, errorChain } = await importHostCapability('@deepseek-ai/dsh-llm')
11
+ signal.throwIfAborted()
12
+ const session = agent.session, nodes = session.surface.nodes
13
+ const start = nodes.indexOf(range.start), end = nodes.indexOf(range.end)
14
+ if (start < 0 || end < start || !pairing.toolPairingBalancedBefore(session, range.start)
15
+ || !pairing.toolPairingBalancedAfter(session, range.end)) throw new Error('idle compaction: invalid balanced range')
16
+ const history = historyEvents(session), bySeq = new Map(history.map(event => [event.seq, event]))
17
+ const turn = history.findLast(event => event.type === 'turn/start' || event.type === 'turn/end')
18
+ if (turn?.type === 'turn/start') throw new Error('idle compaction: turn is open')
19
+ const lock = history.findLast(event => ['compaction/start', 'compaction/end', 'session/end-seed'].includes(event.type))
20
+ if (lock?.type === 'compaction/start') throw new Error('idle compaction: already running')
21
+ const seqs = nodes.slice(start, end + 1)
22
+ const selected = meter.measure(session).nodes.slice(start, end + 1)
23
+ if (!isDeepStrictEqual(selected.map(n => n.seq), seqs)) throw new Error('idle compaction: stale pricing')
24
+ const original = seqs.map(seq => bySeq.get(seq))
25
+ if (original.some(event => !event)) throw new Error('idle compaction: source event missing')
26
+ if (original.some(event => event.type === 'system/message')) throw new Error('idle compaction: system head cannot be replaced')
27
+ const messages = original.map(event => session.deriveEventMessage(event)).filter(Boolean)
28
+ const head = bySeq.get(nodes[0])
29
+ if (head?.type === 'system/message') messages.unshift(session.deriveEventMessage(head))
30
+ const header = session.requestHeader()
31
+ const lifecycle = { compactionId: randomUUID(), turn: null }
32
+ const startEvent = session.append('compaction/start', lifecycle)
33
+ let closing = false
34
+ try {
35
+ const summary = await compaction.summarize({ messages, ...(header?.tools ? { tools: header.tools } : {}) }, agent, signal)
36
+ signal.throwIfAborted()
37
+ const current = session.surface.nodes, first = current.indexOf(range.start)
38
+ const currentEvents = new Map(historyEvents(session).map(event => [event.seq, event]))
39
+ if (!isDeepStrictEqual(current.slice(first, first + seqs.length), seqs)
40
+ || !isDeepStrictEqual(seqs.map(seq => currentEvents.get(seq)), original)
41
+ || !isDeepStrictEqual(meter.measure(session).nodes.slice(first, first + seqs.length), selected)) throw new Error('idle compaction: selected surface changed')
42
+ const checkpoint = createUserMessage({ content: frameSummary(summary.summary), source: pairing.compactCheckpointSource(lifecycle.compactionId) })
43
+ if (meter.estimateMessage(checkpoint) >= selected.reduce((sum, node) => sum + node.tokens, 0)) throw new Error('idle compaction: summary is not smaller')
44
+ const shadowedTokenCount = selected.reduce((sum, node) => sum + node.heuristicTokens, 0)
45
+ const summaryEvent = session.append('compaction/summary', { ...summary, compactionId: lifecycle.compactionId,
46
+ shadowedRange: range, shadowedSeqs: seqs, shadowedTokenCount })
47
+ signal.throwIfAborted()
48
+ session.append('user/message', checkpoint, { surfaceOp: { op: 'replace', startSeq: range.start, endSeq: range.end }, sourceEventSeqs: [startEvent.seq, summaryEvent.seq, ...seqs] })
49
+ signal.throwIfAborted()
50
+ closing = true
51
+ const endEvent = session.append('compaction/end', lifecycle)
52
+ return { compactionId: lifecycle.compactionId, startSeq: startEvent.seq, summarySeq: summaryEvent.seq, endSeq: endEvent.seq,
53
+ summary: summary.summary, shadowedRange: range, shadowedSeqs: seqs, shadowedTokenCount }
54
+ } catch (error) {
55
+ if (!closing) { closing = true; session.append('compaction/end', { ...lifecycle, error: errorChain(error) }) }
56
+ throw error
57
+ } finally { await sessions.flush(session) }
58
+ }
@@ -0,0 +1,7 @@
1
+ export interface ContextRuntime {
2
+ compactNow(signal?: AbortSignal): Promise<unknown>
3
+ compactRegion(start: number, end: number, signal?: AbortSignal): Promise<unknown>
4
+ compactRecovery(signal?: AbortSignal): Promise<unknown>
5
+ status(): { state: string; last: unknown }
6
+ }
7
+ export function contextRuntimeOf(agent: object): ContextRuntime | undefined
@@ -0,0 +1,156 @@
1
+ import { Context } from '@deepseek-ai/cordis'
2
+ import { bool, defineConfig, enumOf, int, passthrough } from './fields.mjs'
3
+ import { capabilityAudience } from './agent-capability.mjs'
4
+ import { importHostCapability } from './host-package.mjs'
5
+ import { routeContextRuntime } from './context-routing.mjs'
6
+ import { createProvenancePruner } from './context-pruner.mjs'
7
+ import { createProvenanceCompaction } from './context-summary.mjs'
8
+ import { compactIdleRegion } from './context-idle-compaction.mjs'
9
+ import { historyEvents } from './history.mjs'
10
+
11
+ export const name = 'context-provenance'
12
+ export const inject = ['llm', 'sessions', 'tokenMeter']
13
+ const configObject = passthrough((value, plugin, field) => {
14
+ if (value === undefined) return {}
15
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${plugin}: ${field} must be an object`)
16
+ return value
17
+ })
18
+ export const configContract = defineConfig({
19
+ enabled: bool({ default: false }), audience: enumOf(['all', 'main', 'subagent'], { default: 'all' }),
20
+ auto: bool({ default: true }), modelSwitch: bool({ default: true }), prune: bool({ default: true }),
21
+ basic: configObject, pruner: configObject,
22
+ recoveryRetainTokens: int({ min: 0, default: 1024 }),
23
+ })
24
+ const runtimes = new WeakMap()
25
+ export const contextRuntimeOf = agent => runtimes.get(agent)
26
+
27
+ /** 用真实surface位置与配对边界选区;保留尾部,不把seq当连续整数。 */
28
+ export function compactableRange(session, measurement, retainTokens, pairing) {
29
+ const nodes = measurement.nodes
30
+ const events = new Map(historyEvents(session).map(event => [event.seq, event]))
31
+ if (nodes.some(node => !events.has(node.seq))) throw new Error('context-provenance: surface event missing')
32
+ let first = nodes.findIndex(node => events.get(node.seq).type !== 'system/message')
33
+ if (first < 0) return null
34
+ while (first < nodes.length && !pairing.toolPairingBalancedBefore(session, nodes[first].seq)) first++
35
+ let retained = 0, last = nodes.length - 1
36
+ while (last >= first && retained < retainTokens) retained += nodes[last--].tokens
37
+ while (last >= first && !pairing.toolPairingBalancedAfter(session, nodes[last].seq)) last--
38
+ return last < first ? null : { start: nodes[first].seq, end: nodes[last].seq }
39
+ }
40
+
41
+ /** 宿主依赖是实际适配器接缝,隔离验证可提供同版本官方构造器。 */
42
+ export async function mountContextRuntime(ctx, raw, { agent, sourceModuleId }, packages) {
43
+ const config = configContract.parse(raw, name)
44
+ if (!config.enabled || !agent || config.audience !== 'all' && config.audience !== capabilityAudience(agent)) return
45
+ if (runtimes.has(agent)) throw new Error('context-provenance: Agent already has a runtime owner')
46
+ ctx.fiber.assertActive()
47
+ const { Basic, Pruner, pairing } = packages
48
+ const privateCtx = new Context()
49
+ const lifetime = new AbortController()
50
+ let active = true
51
+ const status = { state: 'ready', last: null }
52
+ const off = []
53
+ let offRoute = () => {}, runtime
54
+ // 先登记所有权再等待构造器,父scope在await期间释放也能取消私有实例。
55
+ const dispose = ctx.effect(() => () => {
56
+ active = false; lifetime.abort(); status.state = 'disabled'
57
+ if (runtime && runtimes.get(agent) === runtime) runtimes.delete(agent)
58
+ offRoute(); for (const remove of off) remove()
59
+ return privateCtx.fiber.dispose()
60
+ })
61
+ const meter = ctx.get('tokenMeter'), llm = ctx.get('llm'), sessions = ctx.get('sessions')
62
+ // 只把公开方法绑定到原服务;私有Context不接管共享服务生命周期。
63
+ privateCtx.provide('tokenMeter', { measure: (...args) => meter.measure(...args), estimateMessage: message => meter.estimateMessage(message) })
64
+ privateCtx.provide('llm', { stream: options => llm.stream(options), resolveModelInfo: (...args) => llm.resolveModelInfo(...args) })
65
+ privateCtx.provide('sessions', { flush: session => sessions.flush(session) })
66
+ const combined = signal => signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal
67
+ const check = signal => { combined(signal).throwIfAborted() }
68
+ const Compaction = createProvenanceCompaction(Basic, { sourceModuleId, isActive: () => active, signalOf: () => lifetime.signal })
69
+ try {
70
+ if (config.prune) {
71
+ const CitedPruner = createProvenancePruner(Pruner, { sourceModuleId, isActive: () => active, signalOf: () => lifetime.signal })
72
+ await privateCtx.plugin(CitedPruner, config.pruner)
73
+ check()
74
+ }
75
+ await privateCtx.plugin(Compaction, { ...config.basic, auto: config.auto })
76
+ check()
77
+ } catch (error) { await dispose(); throw error }
78
+ const compaction = privateCtx.get('compaction'), pruner = privateCtx.get('toolResultPruner')
79
+ const run = async (action, signal) => {
80
+ check(signal); status.state = 'running'
81
+ try {
82
+ const result = await action(combined(signal))
83
+ check(signal)
84
+ status.state = result === null ? 'no-op' : 'success'; status.last = result
85
+ return result
86
+ } catch (error) { status.state = combined(signal).aborted ? 'cancelled' : 'failed'; throw error }
87
+ }
88
+ runtime = {
89
+ status: () => ({ ...status }),
90
+ compactNow: (signal, commandId) => run(abort => compaction.compactNow(agent, abort, commandId), signal),
91
+ compactRegion: (start, end, signal) => run(abort => agent.status === 'idle'
92
+ ? agent.runMaintenance(maintenanceSignal => compactIdleRegion({ agent, range: { start, end },
93
+ signal: AbortSignal.any([abort, maintenanceSignal]), meter, sessions, compaction, pairing }))
94
+ : compaction.compactRegion(start, end, agent, abort), signal),
95
+ compactRecovery: signal => run(abort => agent.runMaintenance(async maintenanceSignal => {
96
+ const operation = AbortSignal.any([abort, maintenanceSignal])
97
+ operation.throwIfAborted()
98
+ const range = compactableRange(agent.session, meter.measure(agent.session), config.recoveryRetainTokens, pairing)
99
+ if (!range) return null
100
+ return compactIdleRegion({ agent, range, signal: operation, meter, sessions, compaction, pairing })
101
+ }), signal),
102
+ }
103
+ try {
104
+ runtimes.set(agent, runtime)
105
+ offRoute = routeContextRuntime(ctx, agent, {
106
+ compaction: {
107
+ compactNow: (_owner, signal, commandId) => runtime.compactNow(signal, commandId),
108
+ compactRegion: (start, end, _owner, signal) => runtime.compactRegion(start, end, signal),
109
+ compactIfNeeded: (_owner, trigger, signal) => run(abort => compaction.compactIfNeeded(agent, trigger, abort), signal),
110
+ },
111
+ toolResultPruner: pruner ? { pruneSession: session => { check(); return pruner.pruneSession(session, lifetime.signal) } } : undefined,
112
+ })
113
+ for (const event of ['agent/pre-step', 'agent/request-error']) off.push(ctx.on(event, async (payload, next) => {
114
+ if (!active || payload.agent !== agent) return next()
115
+ const result = await privateCtx.waterfall(event, { ...payload, signal: combined(payload.signal) }, () => undefined)
116
+ return result === undefined ? next() : result
117
+ }, { prepend: true }))
118
+ // Basic的retry账本按官方durable结算与idle复位。
119
+ off.push(ctx.on('session/event', (session, event) => { if (active && session === agent.session) privateCtx.emit('session/event', session, event) }))
120
+ off.push(ctx.on('agent/status', payload => { if (active && payload.agent === agent) privateCtx.emit('agent/status', payload) }))
121
+ if (config.modelSwitch) off.push(ctx.on('agent/request', async (payload, next) => {
122
+ const request = await next()
123
+ if (!active || payload.agent !== agent || payload.signal?.aborted) return request
124
+ const previous = agent.session.requestHeader()
125
+ if (!previous || previous.config.provider === request.provider && previous.config.model === request.model) return request
126
+ const signal = combined(payload.signal)
127
+ const info = await llm.resolveModelInfo(request.provider, request.model, signal)
128
+ check(signal)
129
+ const policy = { ...compaction.config, ...(compaction.config.modelPolicies?.find(p => p.provider === request.provider && p.model === request.model) ?? {}) }
130
+ const capacity = info.context?.contextWindow
131
+ const reserved = request.maxTokens ?? info.defaultMaxTokens ?? 0
132
+ if (!Number.isFinite(capacity) || capacity <= reserved + policy.headroomTokens) throw new Error('context-provenance: model switch has no message budget')
133
+ const threshold = Math.floor(Math.min(capacity * policy.thresholdRatio, capacity - reserved - policy.headroomTokens))
134
+ const retain = policy.retainTokens ?? Math.floor((capacity - reserved) * policy.retainRatio)
135
+ if (retain >= threshold) throw new Error('context-provenance: model switch retention exceeds budget')
136
+ for (let attempt = 0; ; attempt++) {
137
+ const measurement = meter.measure(agent.session, { ...previous, config: { ...previous.config, ...request } })
138
+ if (measurement.totalTokens < threshold) return request
139
+ if (attempt > policy.compactionRetries) throw new Error('context-provenance: model switch remains above budget')
140
+ const range = compactableRange(agent.session, measurement, retain, pairing)
141
+ if (!range) throw new Error('context-provenance: model switch has no compactable range')
142
+ await runtime.compactRegion(range.start, range.end, signal)
143
+ }
144
+ }, { prepend: true }))
145
+ return dispose
146
+ } catch (error) { await dispose(); throw error }
147
+ }
148
+
149
+ export async function apply(ctx, config, options = {}) {
150
+ const parsed = configContract.parse(config, name)
151
+ if (!parsed.enabled) return
152
+ const [{ BasicCompactionEngine: Basic }, { ToolResultPruner: Pruner }, pairing] = await Promise.all([
153
+ importHostCapability('@deepseek-ai/dsh-compaction-basic'), importHostCapability('@deepseek-ai/dsh-compaction-tool-result-pruner'), importHostCapability('@deepseek-ai/dsh-compaction'),
154
+ ])
155
+ return mountContextRuntime(ctx, parsed, options, { Basic, Pruner, pairing })
156
+ }