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.
- package/README.md +17 -1
- package/engine/actions/catalog.mjs +2 -1
- package/engine/actions/index.mjs +5 -3
- package/engine/actions/shared.mjs +26 -12
- package/engine/actions/tool-visibility.mjs +31 -14
- package/engine/compositions/source/local/context-provenance.yml +9 -0
- package/engine/compositions/source/local/generation-recovery.yml +10 -0
- package/engine/compositions/source/local/retrieval.yml +7 -0
- package/engine/compositions/source/local/soul-ledger.yml +3 -0
- package/engine/compositions/source/local/soul-maintenance.yml +9 -0
- package/engine/conditions/examples.mjs +1 -0
- package/engine/conditions/index.mjs +7 -0
- package/engine/conditions/manifest.mjs +28 -0
- package/engine/context-idle-compaction.mjs +58 -0
- package/engine/context-provenance.d.mts +7 -0
- package/engine/context-provenance.mjs +156 -0
- package/engine/context-pruner.mjs +64 -0
- package/engine/context-routing.mjs +45 -0
- package/engine/context-summary.mjs +80 -0
- package/engine/fillers.mjs +37 -4
- package/engine/generation-recovery-coordinator.mjs +40 -0
- package/engine/generation-recovery-maintenance.mjs +18 -0
- package/engine/generation-recovery.mjs +153 -0
- package/engine/host-package.mjs +13 -0
- package/engine/layers.d.mts +4 -0
- package/engine/layers.mjs +57 -20
- package/engine/present-as.mjs +2 -2
- package/engine/retrieval-reference.d.mts +5 -0
- package/engine/retrieval-reference.mjs +26 -0
- package/engine/retrieval.mjs +17 -0
- package/engine/rule-engine.mjs +1 -0
- package/engine/rule-runtime.d.mts +8 -1
- package/engine/rule-runtime.mjs +18 -10
- package/engine/rule-spec.mjs +4 -2
- package/engine/schema.mjs +1 -1
- package/engine/service-names.d.mts +5 -0
- package/engine/service-names.mjs +5 -0
- package/engine/soul-ledger.mjs +8 -0
- package/engine/soul-maintenance.mjs +20 -0
- package/lib/client.js +16 -6
- package/lib/client.js.map +1 -1
- package/lib/index.mjs +1953 -246
- package/modules/soul/assets/ledger.md +4 -2
- package/modules/soul/assets/manifest.yml +2 -0
- package/modules/soul/assets/reference/00-layers.md +27 -27
- package/modules/soul/assets/reference/02-echo.md +161 -161
- package/modules/soul/assets/reference/05-doctor.md +67 -67
- package/modules/soul/assets/reference/10-echo-anatomy.md +109 -109
- package/modules/soul/assets/reference/ECHO-/346/235/203/345/250/201/345/217/243/345/276/204.md +202 -202
- 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
- package/modules/soul/assets/templates/agents-template.md +3 -0
- package/modules/soul/assets/templates/self-template.md +19 -0
- package/modules/soul/module.yml +71 -13
- package/modules/tool-surface/module.yml +8 -5
- package/package.json +1 -1
- package/skills/dsh-module/reference.md +2 -2
- 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
|
-
|
|
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: '改模型请求参数',
|
package/engine/actions/index.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
-
/**
|
|
48
|
-
|
|
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 (
|
|
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`
|
|
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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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)
|
|
220
|
+
if (state !== undefined) {
|
|
221
|
+
state.data.failOpen = plan.failOpen
|
|
222
|
+
state.data.unlocked = unlocked
|
|
223
|
+
}
|
|
211
224
|
if (plan.failOpen) {
|
|
212
|
-
//
|
|
213
|
-
warnOnce(`${plugin}: tool-visibility action ${label}:
|
|
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
|
|
@@ -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
|
+
}
|