dsh-plugin-tool-management 0.20.2 → 0.21.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/CHANGELOG.md +60 -1
- package/README.md +4 -1
- package/README_EN.md +4 -1
- package/docs/update.md +3 -1
- package/lib/client.js +225 -54
- package/lib/compat/capability-specs.js +60 -1
- package/lib/compat/host-contracts.js +72 -0
- package/lib/compat/host-probe.js +59 -0
- package/lib/compat/host-roots.js +1 -1
- package/lib/compat/probe.js +84 -13
- package/lib/context-inject-frame.js +134 -26
- package/lib/context-inject.js +12 -9
- package/lib/http-api.js +3 -2
- package/lib/http-fence.js +26 -1
- package/lib/index.js +5 -2
- package/lib/mcp/manager.js +6 -1
- package/lib/mcp/state-section.js +75 -34
- package/lib/memories/constants.js +39 -7
- package/lib/memories/projection.js +18 -18
- package/lib/memories/service.js +8 -7
- package/lib/memories/snapshot.js +27 -17
- package/lib/ops/compat.js +64 -2
- package/lib/ops/memory.js +2 -2
- package/lib/ops/prompts.js +30 -2
- package/lib/request-gate.js +5 -4
- package/lib/skills/catalog.js +2 -2
- package/lib/subagents/catalog.js +4 -4
- package/lib/subagents/child-preset.js +120 -0
- package/lib/subagents/persona-parse.js +9 -9
- package/lib/subagents/run-provider.js +218 -62
- package/lib/subagents/tool-filter.js +12 -12
- package/lib/subagents/tools.js +29 -22
- package/lib/tools/deps.js +4 -4
- package/lib/tools/mcp.js +34 -34
- package/lib/tools/memory.js +34 -34
- package/lib/tools/prompt.js +5 -5
- package/lib/tools/remove.js +17 -17
- package/lib/tools/scene.js +66 -66
- package/lib/tools/skills.js +21 -21
- package/lib/tools/subagent.js +13 -13
- package/lib/tools/table.js +2 -2
- package/package.json +25 -24
|
@@ -34,10 +34,22 @@ export const GLOBAL_SCENE = 'global';
|
|
|
34
34
|
export const GLOBAL_SCENE_LABEL = '全局';
|
|
35
35
|
export const SHARED_GROUP = '_shared'; // 保留场景名:公共基线(历史语义,仍可使用)
|
|
36
36
|
// ── 段渲染 ─────────────────────────────────────────────────────────────────
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* 截断标记。**给模型看的一句英文**,不再是 `<!-- truncated -->`(2026-10-18 审查)。
|
|
39
|
+
*
|
|
40
|
+
* 旧值是个 HTML 注释:对人眼不可见(要的就是"别显示"),但模型读的是**原始 markdown** ——
|
|
41
|
+
* 它看到的是一串 `<!-- truncated -->`,既不知道这是什么、也不知道该拿它怎么办。注入正文里
|
|
42
|
+
* 没有"注释"这个概念,凡是进上下文的东西都该是给模型的一句话。
|
|
43
|
+
*
|
|
44
|
+
* 现在的写法说清两件事:有内容没进来(`Truncated`)、进来的是少于全部的(`not shown`)。
|
|
45
|
+
* 具体漏了哪些由段尾的 `## Not injected` 清单回答,标记只在清单本身也放不下时才独自出现。
|
|
46
|
+
*/
|
|
47
|
+
export const TRUNCATION_MARKER = '(Truncated: some entries are not shown.)';
|
|
38
48
|
// 段尾清单:让模型知道自己漏了什么。去掉伞标题后这里也不再挂「场景记忆」前缀 ——
|
|
39
|
-
//
|
|
40
|
-
|
|
49
|
+
// 它紧跟在场景块之后,与段首引导语同一说法。2026-10-18 起两者都是英文(见 SCENE_MEMORY_NOTE)。
|
|
50
|
+
// 每行只有 `场景/名称`:原先还带 `(N B)`,那个字节数对人有用、对模型零动作价值
|
|
51
|
+
// (它不能凭"这条 800 B"决定读不读)—— 2026-10-18 审查按"注入只写模型能用的信息"删掉。
|
|
52
|
+
export const DROPPED_HEADING = '## Not injected (over the size limit)';
|
|
41
53
|
/** 单行正文的最大长度:超过就退回「标题 + 正文块」,避免出现一条几千字符的列表行。 */
|
|
42
54
|
export const INLINE_BODY_MAX = 120;
|
|
43
55
|
/** 附件行里最多列几个文件名;多的只报总数(路径已经给了,缺的名字模型自己列目录即可)。 */
|
|
@@ -63,7 +75,7 @@ export const ATTACHMENT_LIST_MAX = 10;
|
|
|
63
75
|
*
|
|
64
76
|
* 用词:不用「常驻」「注入」这类内部行话(模型没有先验)。
|
|
65
77
|
* ⚠️ 「用『信息』而不是『记忆』」那条**已被 2026-09-23 第四次裁定推翻**(见下):用户
|
|
66
|
-
*
|
|
78
|
+
* 给定的定稿文案里就是「记忆」,而板块标题本来就是 `Current memories` —— 两者同名反而更好,
|
|
67
79
|
* 当时那条理由(「记忆」在系统提示词里指代不明)在板块标题定名之后也不成立了。
|
|
68
80
|
*
|
|
69
81
|
* 2026-09-17 重写(用户:「当前模式会不重视这些提示词」)。上一版的三个毛病都在**授权**
|
|
@@ -110,7 +122,7 @@ export const ATTACHMENT_LIST_MAX = 10;
|
|
|
110
122
|
* 时以实际情况为准、④ 与用户当场说的冲突时以用户为准、⑤ 无关不必提 + 完整性声明(防探测,
|
|
111
123
|
* 见上,那半句是整句里唯一不能删的)。77 → 48 字符(219 → 144 字节),而它**每个场景块各来
|
|
112
124
|
* 一次**,所以省的是 N 倍。删掉的只有修饰与重复:「以下是用户为本机写的记录」→「本机记录」
|
|
113
|
-
* (板块标题 `#
|
|
125
|
+
* (板块标题 `# Current memories` 已经把"这是本机的记忆"说过了),两句"冲突时以…为准"
|
|
114
126
|
* 合成一句。
|
|
115
127
|
*
|
|
116
128
|
* 2026-09-23 第四次裁定(用户直接给定稿文案,逐字):
|
|
@@ -118,7 +130,7 @@ export const ATTACHMENT_LIST_MAX = 10;
|
|
|
118
130
|
* 相对第三版有三处变化,都不是措辞偏好:
|
|
119
131
|
* ① 「本机记录」→「以下是**当前场景对应的**记忆」:说清这是**哪个范围**的记忆。记忆本来
|
|
120
132
|
* 就按场景组织,而这一段列出的条目确实全来自"当前启用的场景 + 两个保留桶" —— 比
|
|
121
|
-
* "本机记录"准确,也解释了为什么下面的条目按 `##
|
|
133
|
+
* "本机记录"准确,也解释了为什么下面的条目按 `## Scene: X` 分组。
|
|
122
134
|
* ② 「用户当场说的」→「用户**目前的对话**」:同一个意思,后者更贴注入发生的场合。
|
|
123
135
|
* ③ **删掉「可能已过期」,以及完整性声明「以下就是全部信息」**。
|
|
124
136
|
* - 「可能已过期」:用户判断它与紧接的「冲突时以实际情况为准」重复 —— 后半句已经把
|
|
@@ -139,12 +151,32 @@ export const ATTACHMENT_LIST_MAX = 10;
|
|
|
139
151
|
* ② **`global` 的分组标题从 `## 场景:全局` 改成 `## 常驻信息`**(见 `sceneHeading`):
|
|
140
152
|
* 全局是恒常生效的保留桶,不是"一个场景",写成场景会让模型以为它是当前场景的另一种
|
|
141
153
|
* 取值。
|
|
154
|
+
*
|
|
155
|
+
* 2026-10-18(用户裁定):**模型侧模板一律英文**,这句引导语的五个功能一个不动 ——
|
|
156
|
+
* ① 这是当前场景对应的记忆 ② 冲突时以实际情况与用户当场说的为准 ③ 无关勿提(不必提及,
|
|
157
|
+
* 不是不采用)④ 完整性声明(防探测)。**用户写的内容(记忆正文、场景名与说明)原样注入。**
|
|
158
|
+
*
|
|
159
|
+
* 同日晚些时候二次收紧(用户:「让 agent 更能注意到信息,信息高效,也不要冗余」):
|
|
160
|
+
* `when` → `where`,`do not bring them up unless relevant` → `bring them up only when relevant`。
|
|
161
|
+
* 后者不是同义改写:否定式读起来像"对内容的态度许可"(正是第三次裁定要拆掉的那个毛病),
|
|
162
|
+
* 正向式说的是"可以提,只是别主动提"。四个功能一个没少,字数少 11 个。
|
|
142
163
|
*/
|
|
143
164
|
/**
|
|
144
165
|
* 记忆段(`memory-manager-catalog`)的授权语:条目是**记录**(权威等级低于"用户当场说的")。
|
|
145
166
|
* 放在**板块层**(第一个场景块之前),整段只出现一次 —— 见 `renderBody` 尾部。
|
|
146
167
|
*/
|
|
147
|
-
export const SCENE_MEMORY_NOTE = '
|
|
168
|
+
export const SCENE_MEMORY_NOTE = '**Memories for the current scene; where they conflict with what you observe or with what the user says in this conversation, trust that instead; bring them up only when relevant; this is the complete set:**';
|
|
169
|
+
/**
|
|
170
|
+
* 同一条授权语,**去掉完整性声明**的版本 —— 只在真发生截断时用(2026-10-18 审查恢复分叉)。
|
|
171
|
+
*
|
|
172
|
+
* 第五次裁定当时判定"真截断时的矛盾可以接受",理由是默认 128 KiB 正常碰不到。但
|
|
173
|
+
* `rulesMaxBytes` 是公开旋钮(`index.ts` 的 config),预算配小时这个矛盾就是**每一条注入都在
|
|
174
|
+
* 说谎**:段首说"这就是全部",段尾的 `## Not injected` 紧接着列出没进来的条目。模型按段首
|
|
175
|
+
* 那句判断"没有别的记忆了",正好是这条声明本来要防的那个误判。
|
|
176
|
+
*
|
|
177
|
+
* 预算仍按全量版预留(`sceneNoteBytes`),而截断态这句更短,所以恢复分叉不会挤掉任何条目。
|
|
178
|
+
*/
|
|
179
|
+
export const SCENE_MEMORY_NOTE_PARTIAL = '**Memories for the current scene; where they conflict with what you observe or with what the user says in this conversation, trust that instead; bring them up only when relevant:**';
|
|
148
180
|
// ── bundle 与索引 ──────────────────────────────────────────────────────────
|
|
149
181
|
// bundle 附件限制(body 走 HTTP JSON + base64,故比技能上传收紧一档)。
|
|
150
182
|
export const MAX_ATTACH_ENTRY_BYTES = 8 << 20; // 单个附件 8 MiB
|
|
@@ -129,38 +129,38 @@ export function sceneLabel(scene, index) {
|
|
|
129
129
|
/**
|
|
130
130
|
* 记忆段里的场景**分组**标题。
|
|
131
131
|
*
|
|
132
|
-
* `global` 是恒常生效的保留桶,不是"一个场景" —— 叫它 `##
|
|
132
|
+
* `global` 是恒常生效的保留桶,不是"一个场景" —— 叫它 `## Scene: 全局` 会让模型以为"全局"
|
|
133
133
|
* 是当前场景的另一种取值(用户 2026-09-23:「全局不需要写成场景」)。所以它单独成一种标题:
|
|
134
|
-
* `##
|
|
134
|
+
* `## Always-on memories`。其余场景仍是 `## Scene: X`。
|
|
135
135
|
*
|
|
136
|
-
* `_shared`(历史保留名)保持 `##
|
|
136
|
+
* `_shared`(历史保留名)保持 `## Scene: _shared` 不动 —— 它只在旧数据里出现,且没有
|
|
137
137
|
* "全局"这种"它不是场景"的语义歧义(它确实曾是一个共享场景)。
|
|
138
138
|
*/
|
|
139
|
-
export const sceneHeading = (scene) => scene === GLOBAL_SCENE ? '##
|
|
139
|
+
export const sceneHeading = (scene) => scene === GLOBAL_SCENE ? '## Always-on memories' : `## Scene: ${sceneLabel(scene)}`;
|
|
140
140
|
/**
|
|
141
|
-
* 场景段(`scene-manager-catalog
|
|
141
|
+
* 场景段(`scene-manager-catalog`)里的一行:`**"<场景名>" — user description: <场景说明>**`。
|
|
142
142
|
*
|
|
143
|
-
* 2026-09-23 用户裁定,替换了原来的两行式(`##
|
|
143
|
+
* 2026-09-23 用户裁定,替换了原来的两行式(`## Scene: X` + `**场景说明:X**`)。三条理由都是
|
|
144
144
|
* 用户看到**实际注入**之后提的:
|
|
145
145
|
* - **场景名只出现一次**。上一版里它出现三次(框架线索"现在处在哪个场景"、引导语里的
|
|
146
|
-
* 「代码」、标题里的 `##
|
|
146
|
+
* 「代码」、标题里的 `## Scene: 代码`),而说明行只是把同一件事换个标签再说一遍。
|
|
147
147
|
* - **场景说明是「标签」,不是「约定」**。它答的是"这个场景是干什么的",用户的实例就是
|
|
148
148
|
* 「写代码」「前端相关」。上一版把它写成"一律照办、覆盖你的默认做法",等于要求模型
|
|
149
149
|
* "照办一个标签" —— 那正是它读不懂这一段的根源。授权语已删,说明只作注解跟在名字后面。
|
|
150
150
|
* - **没填说明就不带后缀**。上一版会补 `defaultSceneDescription` 的默认句,而
|
|
151
151
|
* "用户配置的上下文,当前启用"贴在名字后面,读起来像一句真的说明。
|
|
152
152
|
*
|
|
153
|
-
*
|
|
153
|
+
* `user description:` 这半句是同日追加的(用户:「防止模型误解这是场景名或者其它」):破折号后面
|
|
154
154
|
* 那截的形态本来有歧义 —— `**「代码」—— 写代码**` 读起来像"名字 —— 别名",而它其实是
|
|
155
|
-
*
|
|
156
|
-
* 前缀只在真有描述时出现(没填描述时整截不出现,不存在"
|
|
155
|
+
* **用户写的一句说明**。加这半句把来源点明,模型就不会把它当成场景的另一个名字或某种标识符。
|
|
156
|
+
* 前缀只在真有描述时出现(没填描述时整截不出现,不存在"user description:"后面空着)。
|
|
157
157
|
*
|
|
158
158
|
* 为什么不再用 `##` 标题:这一段只讲"当前启用的是哪个 + 它是什么",一行说完就够。
|
|
159
159
|
* 场景**分组**标题仍在记忆段里用(`sceneHeading`),两段各自承担自己的职责。
|
|
160
160
|
*/
|
|
161
161
|
export const sceneLine = (scene, index) => {
|
|
162
162
|
const described = String(index?.scenes?.[scene]?.description ?? '').replaceAll(/\s+/g, ' ').trim();
|
|
163
|
-
return
|
|
163
|
+
return `**"${sceneLabel(scene)}"${described === '' ? '' : ` — user description: ${described}`}**\n\n`;
|
|
164
164
|
};
|
|
165
165
|
/** 场景渲染顺序:全局 `global` 最先(它的记忆对任何对话都成立,先讲总则),
|
|
166
166
|
* 其次 `_shared`(历史保留名),其余按(索引 scenes.order, 场景名)。 */
|
|
@@ -246,7 +246,7 @@ export function attachmentLine(file) {
|
|
|
246
246
|
return '';
|
|
247
247
|
const shown = names.slice(0, ATTACHMENT_LIST_MAX);
|
|
248
248
|
const more = names.length - shown.length;
|
|
249
|
-
return
|
|
249
|
+
return `Attachments: ${file.bundleDir} (${shown.join(', ')}${more > 0 ? `, ${names.length} total` : ''})`;
|
|
250
250
|
}
|
|
251
251
|
// ── 单条记忆的渲染 ─────────────────────────────────────────────────────────
|
|
252
252
|
/**
|
|
@@ -254,7 +254,7 @@ export function attachmentLine(file) {
|
|
|
254
254
|
*
|
|
255
255
|
* 为什么不是"缩进 2 格就完事"(这是上一版的实现,被实测推翻):markdown 里缩进 ≤3 格的 `#`
|
|
256
256
|
* **仍然算标题**,所以正文里一句手写的 `# 代码/需求` 会渲染成 H1,**盖过容器自己的
|
|
257
|
-
* `##
|
|
257
|
+
* `## Scene: X`** —— 用户看到的就是这个:内容看起来"逃出了条目",与场景标题平级甚至更高。
|
|
258
258
|
* 缩到 4 格能压住标题(块级标记都要求 ≤3 格缩进),但正文里的列表/引用只是被"压平",
|
|
259
259
|
* 视觉上仍与条目正文同级,达不到"明显从属"。
|
|
260
260
|
*
|
|
@@ -279,8 +279,8 @@ export const explicitDescriptionOf = (f) => (f.descriptionDerived || !f.descript
|
|
|
279
279
|
/**
|
|
280
280
|
* 单条信息的渲染形态 —— **能一行就一行,但恒为列表项**。
|
|
281
281
|
*
|
|
282
|
-
* 单行且不长的正文 → `-
|
|
283
|
-
* 多行或过长的正文 → `-
|
|
282
|
+
* 单行且不长的正文 → `- **名称** (描述) — 正文`(与 MCP / 子智能体两个段的列表同形;无显式描述时括号不出现)
|
|
283
|
+
* 多行或过长的正文 → `- **名称** (描述)` + 空行 + **围栏代码块里的正文**(挂在条目下;理由见 `bodyBlock`)
|
|
284
284
|
*
|
|
285
285
|
* 名称恒为标题:它就是这条记忆的身份(工具 id `<场景>/<名称>`、bundle 目录/文件名都以它为准),
|
|
286
286
|
* 用户说「记忆里的 X」、模型再调 `memory_manager_*` 时都对得上号;显式描述是括号注解,不抢标题。
|
|
@@ -293,14 +293,14 @@ export const explicitDescriptionOf = (f) => (f.descriptionDerived || !f.descript
|
|
|
293
293
|
* 为什么要分两种:用户常有十几条「一句话事实」(「提交格式:PDF」),每条都占标题 + 空行 +
|
|
294
294
|
* 正文三行,整段会散成一长串标题;压成一行后十条信息就是十行。多行正文是**用户写的完整
|
|
295
295
|
* Markdown**(可能自带标题、代码块、嵌套列表),放进围栏块挂到条目下 —— 上一版让它以普通
|
|
296
|
-
* 段落参与外围排版,结果正文里的 `#` 会盖过 `##
|
|
296
|
+
* 段落参与外围排版,结果正文里的 `#` 会盖过 `## Scene: X`(见 `bodyBlock` 的说明)。
|
|
297
297
|
*
|
|
298
298
|
* 返回值带 `inline`:调用方据此决定下一条记忆前要不要空行(单行条目连续排列,其余空行分隔)。
|
|
299
299
|
*/
|
|
300
300
|
export function memoryBlock(file) {
|
|
301
301
|
const desc = explicitDescriptionOf(file);
|
|
302
|
-
// 加粗的只有名称:`-
|
|
303
|
-
const title = `**${file.name}**` + (desc === '' ? '' :
|
|
302
|
+
// 加粗的只有名称:`- **名称** (描述) — 正文`,与 MCP 段 `- **server**(N 个工具) — …` 同形。
|
|
303
|
+
const title = `**${file.name}**` + (desc === '' ? '' : ` (${desc})`);
|
|
304
304
|
const text = String(file.body ?? '').trim();
|
|
305
305
|
const inline = text === '' || (!text.includes('\n') && text.length <= INLINE_BODY_MAX);
|
|
306
306
|
const head = text === ''
|
package/lib/memories/service.js
CHANGED
|
@@ -661,9 +661,10 @@ export function createMemoriesService(ctx, deps) {
|
|
|
661
661
|
* sha1 hex):算的是**文件内容**(AGENTS.md 取截断前的原文),是"文件这一版"的身份,
|
|
662
662
|
* 不是"这次注入了什么";界面只拿它当悬停提示,不参与任何判定。
|
|
663
663
|
*
|
|
664
|
-
* `text` 是**模型侧**正文,开头一行
|
|
665
|
-
*
|
|
666
|
-
*
|
|
664
|
+
* `text` 是**模型侧**正文,开头一行 `Instructions from: <展示路径>` —— 与官方 agent-instructions
|
|
665
|
+
* 逐字同形(`render.js` 的 `sectionText`;2026-10-18 由 `Source:` 改过来:官方注入本来就用
|
|
666
|
+
* 这个措辞,模型对它有现成的先验)。模型据此知道这些规则写在哪个文件里:用户说"把这条记下来"
|
|
667
|
+
* 时它知道该改哪份文件,而不是只能凭印象回答。
|
|
667
668
|
*/
|
|
668
669
|
const resolvePrompt = () => {
|
|
669
670
|
// 场景提示词与 AGENTS.md 正文在用户眼里就是同一件事("全局提示词"):场景期间前者取代后者,
|
|
@@ -678,11 +679,11 @@ export function createMemoriesService(ctx, deps) {
|
|
|
678
679
|
const body = Buffer.byteLength(scene.text, 'utf8') <= AGENTS_MD_MAX_BYTES
|
|
679
680
|
? scene.text
|
|
680
681
|
: Buffer.from(scene.text, 'utf8').subarray(0, AGENTS_MD_MAX_BYTES).toString('utf8') +
|
|
681
|
-
`\n\n
|
|
682
|
+
`\n\n(Truncated: the preset body is over ${Math.floor(AGENTS_MD_MAX_BYTES / 1024)} KiB.)`;
|
|
682
683
|
return {
|
|
683
684
|
path,
|
|
684
685
|
digest: createHash('sha1').update(scene.text).digest('hex'),
|
|
685
|
-
text:
|
|
686
|
+
text: `Instructions from: ${path}\n\n${body}`,
|
|
686
687
|
};
|
|
687
688
|
}
|
|
688
689
|
const raw = readGlobalAgentsMdSync();
|
|
@@ -692,8 +693,8 @@ export function createMemoriesService(ctx, deps) {
|
|
|
692
693
|
const body = Buffer.byteLength(raw, 'utf8') <= AGENTS_MD_MAX_BYTES
|
|
693
694
|
? raw
|
|
694
695
|
: Buffer.from(raw, 'utf8').subarray(0, AGENTS_MD_MAX_BYTES).toString('utf8') +
|
|
695
|
-
`\n\n
|
|
696
|
-
return { path, digest: createHash('sha1').update(raw).digest('hex'), text:
|
|
696
|
+
`\n\n(Truncated: the file is over ${Math.floor(AGENTS_MD_MAX_BYTES / 1024)} KiB.)`;
|
|
697
|
+
return { path, digest: createHash('sha1').update(raw).digest('hex'), text: `Instructions from: ${path}\n\n${body}` };
|
|
697
698
|
};
|
|
698
699
|
/**
|
|
699
700
|
* 模型侧的提示词正文(`''` = 没有可注入的内容)。
|
package/lib/memories/snapshot.js
CHANGED
|
@@ -11,7 +11,7 @@ import { readdirSync, statSync } from 'node:fs';
|
|
|
11
11
|
import { readFile } from 'node:fs/promises';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
import { parseSkillDoc } from '../skills/core.js';
|
|
14
|
-
import { MAX_SOURCE_DEPTH, MAX_DIRECTORIES, MAX_ENTRIES, DEFAULT_ORDER, DEFAULT_GROUP_ORDER, GLOBAL_SCENE, TRUNCATION_MARKER, DROPPED_HEADING, SCENE_MEMORY_NOTE, LEGACY_BUNDLE_DOC, bundleDocName, SHARED_GROUP, byteLen, message, isValidGroupSegment } from './constants.js';
|
|
14
|
+
import { MAX_SOURCE_DEPTH, MAX_DIRECTORIES, MAX_ENTRIES, DEFAULT_ORDER, DEFAULT_GROUP_ORDER, GLOBAL_SCENE, TRUNCATION_MARKER, DROPPED_HEADING, SCENE_MEMORY_NOTE, SCENE_MEMORY_NOTE_PARTIAL, LEGACY_BUNDLE_DOC, bundleDocName, SHARED_GROUP, byteLen, message, isValidGroupSegment } from './constants.js';
|
|
15
15
|
import { ensureSceneRecords, resolveActiveScenes, signatureOfIndex, sceneLabel, sceneHeading, sceneLine, compareSceneBuckets, memoryBlock } from './projection.js';
|
|
16
16
|
import { isIndexQuarantined, readIndex, writeIndex, readFileOrThrowSync, isAbsentError } from './index-io.js';
|
|
17
17
|
// 来源发现与投影(扫目录读成条目 / frontmatter 投成规则 / 老布局搬家)2026-10-01 整段搬到
|
|
@@ -326,7 +326,7 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
326
326
|
// 场景块只留标题:**场景说明归场景段**(`renderSceneCatalog`),否则同一句话会在
|
|
327
327
|
// 上下文里出现两遍。记忆段要的是"这些条目属于哪个场景"这个分组标签。
|
|
328
328
|
// `sceneHeading` 只给标题、不自带结尾空行(场景段的 `sceneLine` 才自带),这里补上 ——
|
|
329
|
-
// 否则标题会与紧跟的引导语/条目贴成一行(`##
|
|
329
|
+
// 否则标题会与紧跟的引导语/条目贴成一行(`## Always-on memories**Memories for…**`)。
|
|
330
330
|
const header = sceneHeading(scene) + '\n\n';
|
|
331
331
|
for (const file of sceneFiles) {
|
|
332
332
|
const { text: block, inline } = memoryBlock(file);
|
|
@@ -344,16 +344,16 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
344
344
|
return { text: '', bytes: 0, truncated: probe.truncated, maxBytes, scenes: [], items: [], dropped: [] };
|
|
345
345
|
}
|
|
346
346
|
/**
|
|
347
|
-
* 选中块 → 段正文(同一场景的 `##
|
|
347
|
+
* 选中块 → 段正文(同一场景的 `## Scene: x` 只在首次出现时发出)。
|
|
348
348
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
349
|
+
* 授权语**两版**(2026-10-18 审查恢复分叉,理由见 `SCENE_MEMORY_NOTE_PARTIAL`):不截断时带
|
|
350
|
+
* "this is the complete set",截断时去掉 —— 否则段首的完整性与段尾的 `## Not injected` 直接
|
|
351
|
+
* 打架。预算仍按全量版预留(`sceneNoteBytes()`),截断态那句更短,不会多占。
|
|
352
352
|
*/
|
|
353
|
-
const renderBody = (selected) => {
|
|
353
|
+
const renderBody = (selected, truncated) => {
|
|
354
354
|
if (selected.length === 0)
|
|
355
355
|
return '';
|
|
356
|
-
const note = SCENE_MEMORY_NOTE;
|
|
356
|
+
const note = truncated ? SCENE_MEMORY_NOTE_PARTIAL : SCENE_MEMORY_NOTE;
|
|
357
357
|
const chunks = [];
|
|
358
358
|
let current = null;
|
|
359
359
|
let buf = '';
|
|
@@ -380,7 +380,7 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
380
380
|
// 引导语只出现**一次**,放在板块层(第一个场景块之前)。
|
|
381
381
|
//
|
|
382
382
|
// 为什么挪上来(用户 2026-09-23 看到实际注入后要求):此前它跟着**每个场景块**各来一遍
|
|
383
|
-
// —— 2 个场景就是 272 B ≈68 tok/轮,占记忆段整段的 22%;而且它夹在 `##
|
|
383
|
+
// —— 2 个场景就是 272 B ≈68 tok/轮,占记忆段整段的 22%;而且它夹在 `## Scene: X` 与条目
|
|
384
384
|
// 之间,把"分组"和"内容"隔开了。它管的本来就是整段(授权语讲的是这一整段条目的效力,
|
|
385
385
|
// 不是某个场景的),放在最前面才对得上。
|
|
386
386
|
//
|
|
@@ -401,7 +401,8 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
401
401
|
const lines = [];
|
|
402
402
|
let used = byteLen(head) + byteLen(marker);
|
|
403
403
|
for (const c of missed) {
|
|
404
|
-
|
|
404
|
+
// 只给 `场景/名称`:字节数对人有用、对模型没有动作价值(2026-10-18 审查)。
|
|
405
|
+
const line = `- ${sceneLabelOf(c.scene)}/${c.item.name}\n`;
|
|
405
406
|
if (used + byteLen(line) > space)
|
|
406
407
|
break;
|
|
407
408
|
lines.push(line);
|
|
@@ -410,8 +411,13 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
410
411
|
if (lines.length === 0)
|
|
411
412
|
return byteLen(marker) <= space ? marker : '';
|
|
412
413
|
const rest = missed.length - lines.length;
|
|
413
|
-
const fold = `-
|
|
414
|
-
|
|
414
|
+
const fold = `- ... (${rest} more not listed)\n`;
|
|
415
|
+
// 清单已经列全时**不再加标记**:`## Not injected` 这个标题本身就说清了"有东西没进来",
|
|
416
|
+
// 再补一句 `(Truncated: …)` 只是把同一件事说两遍。标记只在清单**自己也不全**(有没列出的
|
|
417
|
+
// 条目)时才有信息量 —— 注意判据是 `rest > 0`,不是"折叠行放得下":折叠行放不下时清单
|
|
418
|
+
// 同样是残缺的,标记不能跟着一起消失(那会让残缺的清单看起来是全部)。
|
|
419
|
+
const folded = rest > 0 && used + byteLen(fold) <= space;
|
|
420
|
+
const tail = `${head}${lines.join('')}${folded ? fold : ''}${rest > 0 ? marker : ''}`;
|
|
415
421
|
return tail;
|
|
416
422
|
};
|
|
417
423
|
// ── ① 贪心:放得下就放,越界**跳过**(而不是整体停止)──────────────────
|
|
@@ -436,13 +442,17 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
436
442
|
}
|
|
437
443
|
missed.sort((a, b) => a.seq - b.seq);
|
|
438
444
|
// ── ② 尾注自身也占字节:放不下就把已入选的块从后往前退回,直到回到预算内 ──
|
|
439
|
-
|
|
440
|
-
|
|
445
|
+
// 引导语要不要带完整性声明,取决于**最终**有没有截断 —— 所以每轮重算 body(贪心阶段不知道
|
|
446
|
+
// 结论)。两版字数差在 3 个词以内,预算按全量版预留,不会因此挤掉条目。
|
|
447
|
+
let truncatedNow = probe.truncated || missed.length > 0;
|
|
448
|
+
let body = renderBody(selected, truncatedNow);
|
|
449
|
+
let tail = renderTail(missed, maxBytes - byteLen(body), truncatedNow);
|
|
441
450
|
while (byteLen(body) + byteLen(tail) > maxBytes && selected.length > 0) {
|
|
442
451
|
missed.push(selected.pop());
|
|
443
452
|
missed.sort((a, b) => a.seq - b.seq);
|
|
444
|
-
|
|
445
|
-
|
|
453
|
+
truncatedNow = true;
|
|
454
|
+
body = renderBody(selected, truncatedNow);
|
|
455
|
+
tail = renderTail(missed, maxBytes - byteLen(body), truncatedNow);
|
|
446
456
|
}
|
|
447
457
|
let text = `${body}${tail}`.replace(/^\n+/, '');
|
|
448
458
|
// ③ 兜底:预算小到连标记都放不下时也宁可超出几个字节——"有记忆没注入"这件事
|
|
@@ -456,7 +466,7 @@ export function renderSceneMemory(probe, index, maxBytes) {
|
|
|
456
466
|
return {
|
|
457
467
|
text,
|
|
458
468
|
bytes: byteLen(text),
|
|
459
|
-
truncated:
|
|
469
|
+
truncated: truncatedNow,
|
|
460
470
|
maxBytes,
|
|
461
471
|
scenes,
|
|
462
472
|
items: selected.map((c) => c.item),
|
package/lib/ops/compat.js
CHANGED
|
@@ -11,7 +11,22 @@ import { INJECT_DOMAIN_KEYS } from '../context-inject.js';
|
|
|
11
11
|
import { MCP_TOOL_PREFIX } from '../host-names.js';
|
|
12
12
|
import { assessPresetReach } from '../compat/preset-reach.js';
|
|
13
13
|
import { runtimeNotes } from '../compat/runtime-notes.js';
|
|
14
|
+
import { AGENTS_MD_REFRESH_PACKAGE } from '../compat/host-contracts.js';
|
|
14
15
|
import { pluginLog } from '../skills/service.js';
|
|
16
|
+
/**
|
|
17
|
+
* 功能总览里那几条「宿主契约」行对应的**能力 id**(capability-specs.ts 那张表)。
|
|
18
|
+
*
|
|
19
|
+
* 为什么要一张显式映射:`findings` 的 id 是宿主侧的稳定契约名(`agentPreset.register` /
|
|
20
|
+
* `tools.restrict-method` / `agent.created-event` / `connection.admit`),而行 key 是本插件自己
|
|
21
|
+
* 的界面标识(`subagent-persona` / ...)。两边命名空间不同,写成字面量表最直白,也最不容易
|
|
22
|
+
* 在改名时悄悄对错。
|
|
23
|
+
*/
|
|
24
|
+
const CONTRACT_CAPABILITY_IDS = {
|
|
25
|
+
'subagent-persona': 'agentPreset.register',
|
|
26
|
+
'subagent-tool-restrict': 'tools.restrict-method',
|
|
27
|
+
'subagent-created-event': 'agent.created-event',
|
|
28
|
+
'web-admission': 'connection.admit',
|
|
29
|
+
};
|
|
15
30
|
export function buildCompatOps(deps) {
|
|
16
31
|
return {
|
|
17
32
|
'compat-status': async (args) => {
|
|
@@ -72,6 +87,9 @@ export function buildCompatOps(deps) {
|
|
|
72
87
|
// 修复指引按实际形态给:`scripts/` 不在 `files` 里,npm 安装形态下没有
|
|
73
88
|
// `scripts/doctor.mjs` 与 `scripts/host-deps.mjs`,指了也跑不了。
|
|
74
89
|
sourceInstall: isSourceInstall(),
|
|
90
|
+
// 宿主非 peer 契约的实况:插件按裸模块名 require 的那几个官方包。与 findings 不同源
|
|
91
|
+
// (模块层 vs 服务层),所以单独一个字段,界面据此列出"裸模块解析"那一块。
|
|
92
|
+
...(assessment.contracts === undefined ? {} : { contracts: assessment.contracts }),
|
|
75
93
|
findings,
|
|
76
94
|
degraded,
|
|
77
95
|
blockers: assessment.identity.blockers,
|
|
@@ -297,9 +315,53 @@ export function buildCompatOps(deps) {
|
|
|
297
315
|
...(asarHost === undefined ? {} : { asarHost: true, asarName: asarHost.asarName }),
|
|
298
316
|
}),
|
|
299
317
|
});
|
|
300
|
-
//
|
|
318
|
+
// 宿主契约层(0.2.1 新增的两处 + 一个连接面):这几条**不是**上面那些从 0.1.x 起
|
|
319
|
+
// 就在表里的能力,而是本版适配真实依赖的宿主成员。它们的共同点是缺了**不抛错**,
|
|
320
|
+
// 只是功能少一半 —— 所以必须在总览里占一行,让"升级后某处悄悄不工作"有地方看。
|
|
321
|
+
//
|
|
322
|
+
// 逐条的后果(与 capability-specs.ts 里的 fallback 一一对应):
|
|
323
|
+
// · agentPreset.register 没了 → 带人设的子代理委派失败(临时预设注册不上去);
|
|
324
|
+
// · tools.restrict-method 没了 → 人设的 tools/toolsDeny 不生效,子代理继承父级全部工具;
|
|
325
|
+
// · agent.created-event 串不起来 → 工具限制慢一步(首轮看到的还是继承来的工具集);
|
|
326
|
+
// · connection.admit 没了 → Web 鉴权退回 requestRejection,再缺则退回本地回环判定。
|
|
327
|
+
const findingOf = (id) => assessment === undefined ? undefined : assessment.findings.find((item) => item.id === id);
|
|
328
|
+
const contractRows = [
|
|
329
|
+
['subagent-persona', '子智能体人设委派', 'subagents'],
|
|
330
|
+
['subagent-tool-restrict', '子代理工具限制', 'subagents'],
|
|
331
|
+
['subagent-created-event', '子代理创建时序', 'subagents'],
|
|
332
|
+
['web-admission', 'Web 连接准入', 'compat'],
|
|
333
|
+
];
|
|
334
|
+
for (const [key, label, tab] of contractRows) {
|
|
335
|
+
const finding = findingOf(CONTRACT_CAPABILITY_IDS[key] ?? '');
|
|
336
|
+
push({
|
|
337
|
+
key, label, tab,
|
|
338
|
+
state: finding === undefined ? 'unknown' : finding.state === 'ok' ? 'ok' : 'degraded',
|
|
339
|
+
detail: finding === undefined ? '能力探测不可用(归档服务未挂载)' : finding.detail,
|
|
340
|
+
...(finding === undefined ? {} : { capabilityId: finding.id }),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// 无独立装配点的域:这一行的依据必须是**真的查过什么**,不能拿一句"无降级上报"糊过去 ——
|
|
344
|
+
// 那句话对没有上报通道的域恒真(它只说明"没坏到会上报",不说明"现在好不好")。
|
|
345
|
+
// 有宿主模块可核的域(提示词走 AGENTS.md 裸模块,子智能体还要加上人设包)按实况说;
|
|
346
|
+
// 记忆 / 场景档案确实没有可核的宿主模块,那才停留在"无降级上报"。
|
|
347
|
+
const contractFeature = (packageName) => assessment?.contracts?.packages.find((item) => item.package === packageName);
|
|
301
348
|
for (const [key, label, tab] of [['memory', '记忆', 'memory'], ['prompts', '提示词', 'prompts'], ['subagents', '子智能体', 'subagents'], ['scenes', '场景档案', 'scenes']]) {
|
|
302
|
-
|
|
349
|
+
// 子智能体的依据是那三条宿主契约 + AGENTS.md 裸模块(人设正文要走它注入)。
|
|
350
|
+
const persona = contractFeature('@deepseek-ai/dsh-persona');
|
|
351
|
+
const instructions = contractFeature(AGENTS_MD_REFRESH_PACKAGE);
|
|
352
|
+
const deps = key === 'subagents'
|
|
353
|
+
? [persona, instructions].filter((item) => item !== undefined)
|
|
354
|
+
: key === 'prompts' ? [instructions].filter((item) => item !== undefined) : [];
|
|
355
|
+
const broken = deps.filter((item) => item.resolved === null || item.missingMarkers.length > 0);
|
|
356
|
+
push({
|
|
357
|
+
key, label, tab,
|
|
358
|
+
state: broken.length > 0 ? 'degraded' : 'ok',
|
|
359
|
+
detail: broken.length > 0
|
|
360
|
+
? '宿主契约缺失:' + broken.map((item) => item.feature).join('、')
|
|
361
|
+
: deps.length > 0
|
|
362
|
+
? '依赖的宿主模块在位(' + deps.map((item) => item.feature).join('、') + ')'
|
|
363
|
+
: '无降级上报(装配失败会出现在这里)',
|
|
364
|
+
});
|
|
303
365
|
}
|
|
304
366
|
return { ok: true, rows, generatedAt: Date.now() };
|
|
305
367
|
}
|
package/lib/ops/memory.js
CHANGED
|
@@ -9,7 +9,7 @@ import { lstat, mkdir, readFile, rename, rm } from 'node:fs/promises';
|
|
|
9
9
|
import { basename, dirname, join } from 'node:path';
|
|
10
10
|
import { parseSkillDoc, unquote } from '../skills/core.js';
|
|
11
11
|
import { expandUploads, planMemoryImport } from '../imports/upload.js';
|
|
12
|
-
import { MAX_DESCRIPTION_LENGTH, MAX_RULE_BYTES, DEFAULT_ORDER, DEFAULT_GROUP_ORDER, GLOBAL_SCENE, GLOBAL_SCENE_LABEL,
|
|
12
|
+
import { MAX_DESCRIPTION_LENGTH, MAX_RULE_BYTES, DEFAULT_ORDER, DEFAULT_GROUP_ORDER, GLOBAL_SCENE, GLOBAL_SCENE_LABEL, MAX_ATTACH_ENTRY_BYTES, MAX_ATTACH_TOTAL_BYTES, MAX_ATTACH_ENTRIES, LEGACY_BUNDLE_DOC, bundleDocName, SEGMENT_RULE_HINT, isValidGroupSegment, isValidGroupPath, message, fail } from '../memories/constants.js';
|
|
13
13
|
import { normalizeActive, enabledSceneOf, sceneOf, sceneLabel, attachmentSummarySync } from '../memories/projection.js';
|
|
14
14
|
import { writeFileAtomically, writeFileAtomicBinary, readIndex, writeIndex, pathExists } from '../memories/index-io.js';
|
|
15
15
|
import { deriveDescription } from '../memories/snapshot.js';
|
|
@@ -195,7 +195,7 @@ export function buildMemoryOps(rc) {
|
|
|
195
195
|
issues.push({
|
|
196
196
|
severity: 'warning',
|
|
197
197
|
code: 'sceneMemoryTruncated',
|
|
198
|
-
message: `场景记忆段超出预算(${projection.bytes} 字节 > ${projection.maxBytes}
|
|
198
|
+
message: `场景记忆段超出预算(${projection.bytes} 字节 > ${projection.maxBytes} 字节),已按确定性顺序截断,段尾附未注入清单与截断标记;请精简记忆或只保留 _shared。`,
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
return {
|
package/lib/ops/prompts.js
CHANGED
|
@@ -2,7 +2,28 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 与 ops/compat.ts 同一条纪律:只负责 op 实现,依赖显式传参;MCP 写后刷新、场景锁定守卫
|
|
4
4
|
// 仍作用在组装后的整张表上(见 index.ts)。
|
|
5
|
+
import { resolveAgentsHome } from '../skills/homes.js';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
5
8
|
export function buildPromptOps(deps) {
|
|
9
|
+
/**
|
|
10
|
+
* 共享 agents 根的 `AGENTS.md`(`~/.agents/AGENTS.md`)。
|
|
11
|
+
*
|
|
12
|
+
* 0.2.1 起这条文件**也在被注入**:官方 `dsh-agent-instructions` 的用户全局链是
|
|
13
|
+
* `$DSH_HOME/AGENTS.md` → `<agentsHome>/AGENTS.md` 两份并列(内容相同只渲染一次),
|
|
14
|
+
* 而本插件的提示词页只管得到第一份。于是"我改了什么都没变化"有了第二个成因:
|
|
15
|
+
* 真正在说话的是第二份。这里只回答**它在不在、路径在哪**,不提供写入入口 ——
|
|
16
|
+
* 写入会与官方自己的去重/刷新规则互相覆盖,而这份文件的读者还包括别的 agent 工具。
|
|
17
|
+
*/
|
|
18
|
+
const agentsHomeAgentsMd = () => {
|
|
19
|
+
const path = join(resolveAgentsHome(), 'AGENTS.md');
|
|
20
|
+
let exists = false;
|
|
21
|
+
try {
|
|
22
|
+
exists = existsSync(path);
|
|
23
|
+
}
|
|
24
|
+
catch { /* 读不到 = 当作不在,不阻塞列表 */ }
|
|
25
|
+
return { path, exists };
|
|
26
|
+
};
|
|
6
27
|
return {
|
|
7
28
|
// 提示词预设库(由 ../prompts/service.ts 提供)。这里把两件事对齐成用户看到的一份状态:
|
|
8
29
|
// - `active`(界面「生效中」)= **当前真正在起作用的基线**:启用的场景绑了预设就是它,
|
|
@@ -40,7 +61,7 @@ export function buildPromptOps(deps) {
|
|
|
40
61
|
refs: refs ? refs.get(p.id) || [] : [],
|
|
41
62
|
};
|
|
42
63
|
});
|
|
43
|
-
return { ...base, presets, ...(scenePrompt ? { scenePrompt } : {}) };
|
|
64
|
+
return { ...base, presets, ...(scenePrompt ? { scenePrompt } : {}), agentsHomeAgentsMd: agentsHomeAgentsMd() };
|
|
44
65
|
},
|
|
45
66
|
'agentsmd-read': (args) => deps.promptsService.read(String((args && args.id) || '')),
|
|
46
67
|
// content 与 from 都可选:新建弹窗里直接写正文(content),或从现有预设复制(from)。
|
|
@@ -107,7 +128,14 @@ export function buildPromptOps(deps) {
|
|
|
107
128
|
}
|
|
108
129
|
return res;
|
|
109
130
|
},
|
|
110
|
-
|
|
131
|
+
// 与 `agentsmd-list` 同一份事实:两处都要展示"共享 agents 根那一份在不在"(列表用来
|
|
132
|
+
// 提示,当前页用来对照),分成两个实现必然会出现"列表说在、当前页说不在"。
|
|
133
|
+
'agentsmd-get-current': async () => {
|
|
134
|
+
const res = await deps.promptsService.getCurrent();
|
|
135
|
+
if (!res || res.ok === false)
|
|
136
|
+
return res;
|
|
137
|
+
return { ...res, agentsHomeAgentsMd: agentsHomeAgentsMd() };
|
|
138
|
+
},
|
|
111
139
|
// 删除:**场景绑定**与**当前应用的那份**拦得住(判据见下方注释)。拒绝时逐条说明「谁在用」,
|
|
112
140
|
// 用户知道该先改哪里。引用清单由 scene-prompt-sync 与显示/注入同源算出。
|
|
113
141
|
//
|
package/lib/request-gate.js
CHANGED
|
@@ -31,9 +31,10 @@ export function createAccessToken(deps) {
|
|
|
31
31
|
// 2. defense in depth — when set, every state-changing op additionally
|
|
32
32
|
// requires `x-dsh-token: <token>`.
|
|
33
33
|
// It is NOT the primary gate anymore: the route calls the host's
|
|
34
|
-
// connection.
|
|
35
|
-
//
|
|
36
|
-
// so an unset token no longer
|
|
34
|
+
// connection.admit fence first (Host must be loopback/LAN IP-literal →
|
|
35
|
+
// defeats DNS rebinding, plus browser-session cookie auth, plus the
|
|
36
|
+
// owner-Principal check an exact route owes), so an unset token no longer
|
|
37
|
+
// means "anyone may call writes".
|
|
37
38
|
// NOTE: the entry config arrives as the SECOND apply argument (Cordis
|
|
38
39
|
// calls `callback(ctx, config)`) — never read it off `ctx.config`, which
|
|
39
40
|
// is not an injected service and throws "cannot get property without
|
|
@@ -151,7 +152,7 @@ export function createSceneLock(deps) {
|
|
|
151
152
|
if (!scene)
|
|
152
153
|
return null;
|
|
153
154
|
const locked = await lockedSceneNames();
|
|
154
|
-
return locked.includes(scene) ?
|
|
155
|
+
return locked.includes(scene) ? `scene "${scene}" is locked, so this write was refused: that domain cannot be modified while the scene is locked; the user has to unlock it first.` : null;
|
|
155
156
|
}
|
|
156
157
|
return { lockedSceneNames, activeSceneName, lockedSceneGuard };
|
|
157
158
|
}
|
package/lib/skills/catalog.js
CHANGED
|
@@ -81,8 +81,8 @@ export function renderSkillCatalog(data, maxEntries = SKILL_CATALOG_MAX_ENTRIES,
|
|
|
81
81
|
// 它只会去猜名字。数量照报 —— 那是实话,与有没有出口无关。
|
|
82
82
|
if (hidden > 0) {
|
|
83
83
|
out.push('', listToolVisible
|
|
84
|
-
?
|
|
85
|
-
:
|
|
84
|
+
? `(${hidden} more not listed — see them with \`skill_manager_list\`.)`
|
|
85
|
+
: `(${hidden} more not listed.)`);
|
|
86
86
|
}
|
|
87
87
|
return out.join('\n');
|
|
88
88
|
}
|
package/lib/subagents/catalog.js
CHANGED
|
@@ -13,7 +13,7 @@ export const DEFAULT_CATALOG_DESCRIPTION_MAX_LENGTH = 500;
|
|
|
13
13
|
/** 段里最多列几个人设;超出部分只报数量,让模型自己去调 subagent_manager_list。 */
|
|
14
14
|
export const DEFAULT_CATALOG_MAX_ENTRIES = 50;
|
|
15
15
|
/** 无描述时的占位,与 `subagent_manager_list` 工具的输出保持同一口径。 */
|
|
16
|
-
const NO_DESCRIPTION = '(
|
|
16
|
+
const NO_DESCRIPTION = '(no description)';
|
|
17
17
|
/**
|
|
18
18
|
* 描述归一化 + 截断 —— 逐字对齐官方 `dsh-tool-skill` 的实现:
|
|
19
19
|
* 先把所有空白折叠成单空格,再按长度截断并补省略号。保证「一人设一行」。
|
|
@@ -57,12 +57,12 @@ export function renderSubagentCatalog(allowed, maxEntries = DEFAULT_CATALOG_MAX_
|
|
|
57
57
|
const out = [...lines];
|
|
58
58
|
// 查询工具**只在真被 40 条上限截掉时**才出现:常态下不提,省常驻字符,也免得模型为了
|
|
59
59
|
// 「确认一遍」去调它(用户裁定:没列出来的就是当前不想要的)。与 MCP 状态段的
|
|
60
|
-
//
|
|
60
|
+
// `(${N} more not listed.)` 同一句式,但这里多给一个出口——不给人设就真的找不回来了。
|
|
61
61
|
if (hidden > 0) {
|
|
62
62
|
// 与技能目录同一条纪律:点名一个被关掉的工具只会让模型去猜名字。数量照报。
|
|
63
63
|
out.push('', listToolVisible
|
|
64
|
-
?
|
|
65
|
-
:
|
|
64
|
+
? `(${hidden} more not listed — see them with \`subagent_manager_list\`.)`
|
|
65
|
+
: `(${hidden} more not listed.)`);
|
|
66
66
|
}
|
|
67
67
|
return out.join('\n');
|
|
68
68
|
}
|