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.
Files changed (42) hide show
  1. package/CHANGELOG.md +60 -1
  2. package/README.md +4 -1
  3. package/README_EN.md +4 -1
  4. package/docs/update.md +3 -1
  5. package/lib/client.js +225 -54
  6. package/lib/compat/capability-specs.js +60 -1
  7. package/lib/compat/host-contracts.js +72 -0
  8. package/lib/compat/host-probe.js +59 -0
  9. package/lib/compat/host-roots.js +1 -1
  10. package/lib/compat/probe.js +84 -13
  11. package/lib/context-inject-frame.js +134 -26
  12. package/lib/context-inject.js +12 -9
  13. package/lib/http-api.js +3 -2
  14. package/lib/http-fence.js +26 -1
  15. package/lib/index.js +5 -2
  16. package/lib/mcp/manager.js +6 -1
  17. package/lib/mcp/state-section.js +75 -34
  18. package/lib/memories/constants.js +39 -7
  19. package/lib/memories/projection.js +18 -18
  20. package/lib/memories/service.js +8 -7
  21. package/lib/memories/snapshot.js +27 -17
  22. package/lib/ops/compat.js +64 -2
  23. package/lib/ops/memory.js +2 -2
  24. package/lib/ops/prompts.js +30 -2
  25. package/lib/request-gate.js +5 -4
  26. package/lib/skills/catalog.js +2 -2
  27. package/lib/subagents/catalog.js +4 -4
  28. package/lib/subagents/child-preset.js +120 -0
  29. package/lib/subagents/persona-parse.js +9 -9
  30. package/lib/subagents/run-provider.js +218 -62
  31. package/lib/subagents/tool-filter.js +12 -12
  32. package/lib/subagents/tools.js +29 -22
  33. package/lib/tools/deps.js +4 -4
  34. package/lib/tools/mcp.js +34 -34
  35. package/lib/tools/memory.js +34 -34
  36. package/lib/tools/prompt.js +5 -5
  37. package/lib/tools/remove.js +17 -17
  38. package/lib/tools/scene.js +66 -66
  39. package/lib/tools/skills.js +21 -21
  40. package/lib/tools/subagent.js +13 -13
  41. package/lib/tools/table.js +2 -2
  42. package/package.json +25 -24
@@ -29,14 +29,14 @@ export async function filterBySceneBinding(docs, enabledSceneLists, includeUnava
29
29
  if (includeUnavailable || !bound.size)
30
30
  return { allowed: live, reason: null };
31
31
  const allowed = live.filter((d) => bound.has(d.name));
32
- return { allowed, reason: allowed.length ? null : `当前启用场景的子智能体绑定: ${[...bound].join('、')}` };
32
+ return { allowed, reason: allowed.length ? null : `the enabled scene's subagent bindings: ${[...bound].join(', ')}` };
33
33
  }
34
34
  export function defineSubagentManagerListTool(subagents) {
35
35
  return {
36
36
  name: 'subagent_manager_list',
37
37
  // `depth-filtered` 删掉(2026-10-15 注入文本审查):注入深度是**注入侧**的实现细节,
38
38
  // 模型看到的是"目录里有哪些人设",`catalogDepth` 那个词对它没有任何可操作性。
39
- description: 'List personas (pre-configured subagent profiles) with their descriptions. The「当前子智能体」reminder carries a view of the same catalog; call this for the always-current full list, or pass all=true to include personas that are disabled or outside the active scene.',
39
+ description: 'List personas (pre-configured subagent profiles) with their descriptions. The "Current subagents" reminder carries a view of the same catalog; call this for the always-current full list, or pass all=true to include personas that are disabled or outside the active scene.',
40
40
  parameters: {
41
41
  all: { type: 'boolean', description: 'Include disabled personas and ones outside the active scene (default false). Listed ≠ delegable: `run` still refuses those.' },
42
42
  },
@@ -54,9 +54,9 @@ export function defineSubagentManagerListTool(subagents) {
54
54
  // 而工具又不列全,结果是"明明能委派却查不到人设"。
55
55
  // `all=true` 时**标出哪些不可用**:只列名字的话,模型照样分不清哪个能直接委派
56
56
  // (与技能列表标 `shadowed-by:` 同一条理由 —— 列出来不等于能用)。
57
- const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(无描述)') +
57
+ const lines = allowed.map((p) => '- ' + p.name + ' — ' + (p.description || '(no description)') +
58
58
  (p.continuable === true ? CONTINUABLE_MARK : '') +
59
- (includeUnavailable ? (p.enabled === false ? '(已停用,需先启用)' : '') : ''));
59
+ (includeUnavailable ? (p.enabled === false ? ' (disabled — turn it on first)' : '') : ''));
60
60
  // 默认口径下有多少个没列出来(停用的 + 不在启用场景里的):不报的话,模型看到一份短名单
61
61
  // 会当成"本机只有这些人设"。数量照报,与另外三个 list 的表头同一口径。
62
62
  const hiddenCount = includeUnavailable ? 0 : docs.length - allowed.length;
@@ -67,9 +67,9 @@ export function defineSubagentManagerListTool(subagents) {
67
67
  catch {
68
68
  notice = '';
69
69
  }
70
- const header = '人设子智能体(' + allowed.length + ')' +
71
- (hiddenCount > 0 ? '(另有 ' + hiddenCount + ' 个停用或不在当前场景,传 all=true 查看)' : '') + ':\n';
72
- return header + (lines.join('\n') || '(无)') + (reason ? '\n注意:' + reason : '') + notice;
70
+ const header = 'persona subagents (' + allowed.length + ')' +
71
+ (hiddenCount > 0 ? ' (' + hiddenCount + ' more are disabled or outside the current scene; pass all=true to see them)' : '') + ':\n';
72
+ return header + (lines.join('\n') || '(none)') + (reason ? '\nnote: ' + reason : '') + notice;
73
73
  },
74
74
  };
75
75
  }
@@ -105,9 +105,9 @@ export function defineSubagentManagerRunTool(subagents) {
105
105
  // 最贵的单个参数。收完两处重复后目标 ≈325 tok。判据是"删掉的那句在描述里已经有了吗":
106
106
  // Modes 段留着(它是模型的入口),参数说明只留"这一段独有的信息"。
107
107
  // `inherit` 那句"轮中委派拿不到当前轮"必须留 —— 它是这条参数唯一会让人写错 task 的地方。
108
- description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output. A persona marked「允许追问」instead returns an agent id and runs in the background — continue it with `send_message`.\n\nModes: by default a fresh child that cannot see this conversation, so `task` must be self-contained. With `inherit: true` it also gets this conversation\'s **finished** turns (like the host\'s `subagent_fork`) — the current turn is never included, so a mid-turn hand-off still needs a self-contained `task`.\n\n`task` = the goal plus the context it needs; leave method and output format to the persona.\n\nUse it when the work matches a persona in the「当前子智能体」reminder (a review, an investigation, a piece of writing) and the detail should not sit in your own context. Not for reading a file (Read), finding a definition (Grep/Glob), or touching two or three files.',
108
+ description: 'Run a named persona as a subagent: it gets the persona as its own system prompt, works on `task`, and returns only its final output. A persona marked `(continuable)` instead returns an agent id and runs in the background — continue it with `send_message`.\n\nModes: by default a fresh child that cannot see this conversation, so `task` must be self-contained. With `inherit: true` it also gets this conversation\'s **finished** turns (like the host\'s `subagent_fork`) — the current turn is never included, so a mid-turn hand-off still needs a self-contained `task`.\n\n`task` = the goal plus the context it needs; leave method and output format to the persona.\n\nUse it when the work matches a persona in the "Current subagents" reminder (a review, an investigation, a piece of writing) and the detail should not sit in your own context. Not for reading a file (Read), finding a definition (Grep/Glob), or touching two or three files.',
109
109
  parameters: {
110
- agent: { type: 'string', required: true, description: 'Persona name — see the「当前子智能体」reminder, or list personas with subagent_manager_list.' },
110
+ agent: { type: 'string', required: true, description: 'Persona name — see the "Current subagents" reminder, or list personas with subagent_manager_list.' },
111
111
  // `task` 的说明只留"这是什么"(2026-10-15 注入文本审查):上面描述里那句
112
112
  // "leave method and output format to the persona." 此前在这里**逐字重复**了一遍
113
113
  // (57 字符),而模型填这个参数时两处都在手上 —— 同一件事付两次 token。
@@ -123,22 +123,23 @@ export function defineSubagentManagerRunTool(subagents) {
123
123
  const task = String((args && args.task) || '').trim();
124
124
  const inherit = args && args.inherit === true;
125
125
  if (!task)
126
- throw new Error('task 不能为空');
126
+ throw new Error('task must not be empty');
127
127
  // 官方 SubagentStartRequest.parent 必填:调用方 agent 缺失时给结构化错误,不把 undefined 透传下去。
128
128
  if (!exec || !exec.agent)
129
- throw new Error('缺少调用方 agent(exec.agent 不可用):无法创建子代理运行');
129
+ throw new Error('no caller agent (exec.agent is unavailable): cannot create a subagent run');
130
130
  const docs = await subagents.list();
131
131
  const { allowed, reason } = await filterBySceneBinding(docs, await subagents.sceneLists());
132
132
  const persona = allowed.find((p) => p.name === name);
133
133
  if (!persona) {
134
- const available = allowed.map((p) => p.name).join('、') || '(无)';
135
- throw new Error('人设不可用: ' + name + (reason ? '(' + reason + ')' : '(可用: ' + available + ')'));
134
+ const available = allowed.map((p) => p.name).join(', ') || '(none)';
135
+ throw new Error('persona unavailable: ' + name + (reason ? ' (' + reason + ')' : ' (available: ' + available + ')'));
136
136
  }
137
137
  // 这里**刻意没有**"深度不够就拒绝"的检查(2026-09-17 用户裁定,方案 A)。
138
138
  // 它此前基于一个错误假设:以为传 `maxDepth: 1` 会让子会话再委派必然失败。实际上官方
139
- // `dsh-tool-subagent` 的默认是 **3**、provider 只在**传了值**时才校验,所以子代理本来
140
- // 就能继续嵌套。那个检查的唯一净效果是让**我们的**工具比官方严 —— 同一个子会话里官方
141
- // 工具能委派、我们的被自己拦下 —— 而用户真想禁止嵌套也禁止不了(官方工具照样能)。
139
+ // 的 `maxDepth` 只用**宿主配置**那一档(0.2.1 默认 1)、provider 只在**传了值**时才校验,
140
+ // 所以能不能继续委派问官方就行。那个检查的唯一净效果是让**我们的**工具比官方严 ——
141
+ // 同一个子会话里官方工具能委派、我们的被自己拦下 —— 而用户真想禁止嵌套也禁止不了
142
+ // (官方工具照样能)。
142
143
  // `catalogDepth` 现在只管"目录注入到哪些会话",不参与委派可行性判断。
143
144
  // 按当前会话的 Agent 预设算工具限制:判断不了当前预设、或名单里的工具已经不存在时,
144
145
  // 结论里会带一句实话,跟着结果一起返回——不静默改变限制的强度。
@@ -149,14 +150,20 @@ export function defineSubagentManagerRunTool(subagents) {
149
150
  const prefix = r.stopReason && r.stopReason !== 'completed' ? `[stopReason: ${r.stopReason}]\n` : '';
150
151
  const note = decision && decision.note ? `⚠ ${decision.note}\n\n` : '';
151
152
  // 允许追问通道(人设 `continuable: true`):产出是**异步**到达的,这里只拿到子会话 id。
152
- // 回执必须把"id + 怎么追问 + 怎么中止"三件事写全 —— 少写一句,模型就会把"没拿到结果"
153
- // 读成"委派失败"而重试一次(那会再起一个后台子代理,白白翻倍)。
153
+ // 回执必须把"id + 产出怎么回来 + 怎么追问 + 怎么中止"写全 —— 少写一句,模型就会把
154
+ // "没拿到结果"读成"委派失败"而重试一次(那会再起一个后台子代理,白白翻倍)。
155
+ // "产出怎么回来"说的是**宿主的结算通知**(0.2.1 核实 `dsh-subagent/lib/types/manager.js`
156
+ // 的 `notifySettlement` → `createSettlementMessage`):子代理结束时父代理会收到一条
157
+ // `subagent-settled` 通知,含收尾一行说明 + 子代理最终的非空 text 块(+ 结构化结果与
158
+ // 诊断)。它自己发回来的 `send_message` 是**补充**(`withContinuableReturnGuidance`
159
+ // 要求它在收尾前把自包含结果发给父代理),不是产出的唯一来路 —— 回执不能只说前者,
160
+ // 否则子代理没主动发消息时父代理会把"还没收到结果"当成"还在跑"。
154
161
  if (r.continuable === true) {
155
- return note + '已启动允许追问的子代理「' + name + '」(后台运行)\n'
162
+ return note + 'started the continuable subagent "' + name + '" (running in the background)\n'
156
163
  + 'agent_id: ' + r.runId + '\n\n'
157
- + '它的产出稍后自己发到这里,届时你会收到一条通知。要追问就 '
158
- + 'send_message({ agent_id: "' + r.runId + '", message: "..." }),'
159
- + '要中止就 interrupt_agent({ agent_id: "' + r.runId + '" })。';
164
+ + 'you will get a notification carrying its final output when it settles. To follow up: '
165
+ + 'send_message({ agent_id: "' + r.runId + '", message: "..." }), '
166
+ + 'To stop it: interrupt_agent({ agent_id: "' + r.runId + '" }).';
160
167
  }
161
168
  return note + prefix + r.text;
162
169
  },
package/lib/tools/deps.js CHANGED
@@ -7,7 +7,7 @@ export function text(value) {
7
7
  return [{ type: 'text', text: value }];
8
8
  }
9
9
  /**
10
- * 名单渲染:超过 `TOOL_NAME_LIST_CAP` 项就截断,并把总数写在后面("等 N 项")。
10
+ * 名单渲染:超过 `TOOL_NAME_LIST_CAP` 项就截断,并把总数写在后面("…(N total)")。
11
11
  *
12
12
  * 为什么两个地方都要它:`scene_manager_list` 用它列档案,`scene_manager_save` 用它
13
13
  * 在回执里回显"这一次替换挤掉了什么"。两处都必须**点名、不能只给数量** —— 只给数量
@@ -17,9 +17,9 @@ export function text(value) {
17
17
  export const TOOL_NAME_LIST_CAP = 6;
18
18
  export function toolNameList(list) {
19
19
  if (!list.length)
20
- return '空';
21
- const head = list.slice(0, TOOL_NAME_LIST_CAP).join('、');
22
- return list.length > TOOL_NAME_LIST_CAP ? head + ' 等 ' + list.length + ' 项' : head;
20
+ return 'none';
21
+ const head = list.slice(0, TOOL_NAME_LIST_CAP).join(', ');
22
+ return list.length > TOOL_NAME_LIST_CAP ? head + ' ...(' + list.length + ' total)' : head;
23
23
  }
24
24
  /**
25
25
  * 按名字找技能落在哪个来源(root)。
package/lib/tools/mcp.js CHANGED
@@ -28,7 +28,7 @@
28
28
  import { DEFAULT_MCP_NOTE_MAX_LENGTH, normalizeMcpNote } from '../mcp/state-section.js';
29
29
  import { text } from './deps.js';
30
30
  /** 「去看清单」那句:`mcp_manager_list` 不在模型手里时不能点名它(调用方按 `toolVisibleFor` 决定给不给)。 */
31
- const MCP_LIST_HINT = '(用 mcp_manager_list 看清单,all=true 连停用的也列)';
31
+ const MCP_LIST_HINT = ' (see mcp_manager_list — all=true also lists disabled ones)';
32
32
  /**
33
33
  * 指不到 `mcp_manager_list` 时的**中性出口**:不点名任何工具,两条常驻来源都成立
34
34
  * —— 模型自己的 `mcp__<server>__<tool>` 工具名(就在它的 schema 里),以及注入段。
@@ -37,22 +37,22 @@ const MCP_LIST_HINT = '(用 mcp_manager_list 看清单,all=true 连停用的
37
37
  * 的 MCP 工具,猜错 serverName 时若报错里只有「没有这台服务器:X」,模型拿不到任何补救线索
38
38
  * —— 它只能猜第二次。出口要么指向工具(那条工具得在它手里),要么指向这两条常驻来源。
39
39
  */
40
- const MCP_NEUTRAL_HINT = '(服务器名照抄你自己 `mcp__<server>__<tool>` 工具名里的那一段,或「当前 MCP 服务器」段里列出的名字)';
40
+ const MCP_NEUTRAL_HINT = ' (copy the server name from your own `mcp__<server>__<tool>` tool names, or take one listed in the "Current MCP servers" reminder)';
41
41
  /** 按 serverName 定位条目;同名(project + global 各一台)时要求 level 明确指定。 */
42
42
  function locate(rows, server, level, listHint = MCP_LIST_HINT) {
43
43
  const hits = (rows || []).filter((r) => String(r.serverName) === server);
44
44
  if (!hits.length) {
45
- throw new Error('没有这台服务器:' + server + (listHint || MCP_NEUTRAL_HINT));
45
+ throw new Error('no such server: ' + server + (listHint || MCP_NEUTRAL_HINT));
46
46
  }
47
47
  if (level) {
48
48
  const picked = hits.filter((r) => r.level === level);
49
49
  if (!picked.length) {
50
- throw new Error('「' + server + '」没有 level=' + level + ' 的那一台(现有:' + hits.map((r) => r.level).join(' / ') + ')');
50
+ throw new Error('"' + server + '" has no level=' + level + ' entry (existing: ' + hits.map((r) => r.level).join(' / ') + ')');
51
51
  }
52
52
  return picked[0];
53
53
  }
54
54
  if (hits.length > 1) {
55
- throw new Error('「' + server + '」在 ' + hits.map((r) => r.level).join(' 与 ') + ' 各有一台,无法确定动哪一台 —— 传 level 指定');
55
+ throw new Error('"' + server + '" exists at both ' + hits.map((r) => r.level).join(' and ') + ' — pass level to say which one to touch');
56
56
  }
57
57
  return hits[0];
58
58
  }
@@ -89,11 +89,11 @@ export function buildMcpTools(deps) {
89
89
  // 是行动依据 —— 模型要知道"停用的那台去哪查"就得靠这句(all=true)。
90
90
  //
91
91
  // 2026-10-15 注入文本审查:删掉两处白付的 token ——
92
- // ① 括号里那句把「当前 MCP 服务器」reminder 的**内容**逐项列了一遍(可用 / 曾连上 /
92
+ // ① 括号里那句把 "Current MCP servers" reminder 的**内容**逐项列了一遍(可用 / 曾连上 /
93
93
  // 备注 / 停用不进),而那段 reminder 每轮就在模型上下文里,它自己看得到;
94
94
  // ② `where loader:on:<phase> is the loader's current phase` —— 用自己解释自己
95
95
  // (phase 就是 phase),零信息。字段名本身留在输出里,模型照抄即可。
96
- description: 'List configured MCP servers (level project|global; enabled state; live loader status; tool count excluding switched-off tools; user hint). The「当前 MCP 服务器」reminder is a filtered view; this tool is the raw one — defaults to enabled servers, all=true for every configured one. Pass tools=true to list each server\'s tool names.',
96
+ description: 'List configured MCP servers (level project|global; enabled state; live loader status; tool count excluding switched-off tools; user hint). The "Current MCP servers" reminder is a filtered view; this tool is the raw one — defaults to enabled servers, all=true for every configured one. Pass tools=true to list each server\'s tool names.',
97
97
  parameters: {
98
98
  all: { type: 'boolean', description: 'Include disabled servers (default false).' },
99
99
  tools: { type: 'boolean', description: 'Also list each server\'s tool names, marking the switched-off ones (default false).' },
@@ -162,7 +162,7 @@ export function buildMcpTools(deps) {
162
162
  // 报错退化成「没有这台服务器:<你猜的>」—— 一个字都不告诉它还能去哪看。
163
163
  // 指向模型自己的工具表(`mcp__<server>__<tool>` 就在它的 schema 里)与常驻注入段,
164
164
  // 这两条**不需要任何工具可见**就成立。
165
- server: { type: 'string', required: true, description: 'Server name, as it appears in your own `mcp__<server>__<tool>` tool names (the「当前 MCP 服务器」reminder lists the usable ones).' },
165
+ server: { type: 'string', required: true, description: 'Server name, as it appears in your own `mcp__<server>__<tool>` tool names (the "Current MCP servers" reminder lists the usable ones).' },
166
166
  action: { type: 'string', required: true, enum: ['on', 'off', 'restart'], description: 'on = enable, off = disable, restart = reconnect and re-sync tools.' },
167
167
  level: { type: 'string', description: 'project or global — only to disambiguate same-name servers.' },
168
168
  tool: { type: 'string', description: 'One tool name on that server (no mcp__ prefix). Only for on/off.' },
@@ -171,12 +171,12 @@ export function buildMcpTools(deps) {
171
171
  async execute(args, exec) {
172
172
  const action = String((args && args.action) || '');
173
173
  if (action !== 'on' && action !== 'off' && action !== 'restart') {
174
- throw new Error('action 只能是 on / off / restart(收到:' + (action || '(空)') + ')');
174
+ throw new Error('action must be on / off / restart (got: ' + (action || '(empty)') + ')');
175
175
  }
176
176
  const server = String((args && args.server) || '').trim();
177
177
  const tool = args && args.tool !== undefined ? String(args.tool).trim() : '';
178
178
  if (tool !== '' && action === 'restart') {
179
- throw new Error('重启是整台服务器的动作(先停用、等断开、再按原状态启用),不能只重启一个工具');
179
+ throw new Error('restart is a whole-server action (off, wait for it to drop, then back on at its previous state); a single tool cannot be restarted');
180
180
  }
181
181
  // on|off 会写补丁(`mcpm-set-enabled` / `mcpm-tool-enabled` 在登记表里是 frozen),
182
182
  // 所以过场景冻结守卫;restart 不过 —— 它是锁定期间唯一的恢复入口(见文件头)。
@@ -187,36 +187,36 @@ export function buildMcpTools(deps) {
187
187
  }
188
188
  const list = await deps.mcpmListView();
189
189
  if (!list || list.ok === false)
190
- throw new Error((list && list.error) || '读取 MCP 配置失败');
190
+ throw new Error((list && list.error) || 'failed to read the MCP config');
191
191
  const level = args && args.level !== undefined ? String(args.level) : undefined;
192
192
  const row = locate(list.rows || [], server, level, listHint(exec && exec.agent));
193
193
  const enabled = action === 'on';
194
194
  if (tool !== '') {
195
195
  const r = await deps.mcpmToolEnabled({ serverName: String(row.serverName), tool, enabled });
196
196
  if (!r || r.ok === false)
197
- throw new Error((r && r.error) || '切换工具启停失败');
197
+ throw new Error((r && r.error) || 'failed to switch this tool');
198
198
  // 与面板同一条同步:`mcpm-tool-enabled` 在登记表里带 syncsArchive,漏了就是
199
199
  // "页面拨的进档案、模型拨的不进"。
200
200
  const syncErr = await deps.syncSwitchToScene('mcpm-tool-enabled', { serverName: String(row.serverName), tool, enabled });
201
201
  // 整台停用时,工具级开关在运行时本来就不生效(那个工具压根不存在)—— 不说的话
202
202
  // 模型会以为已经生效。与记忆族"启用了一条不在启用场景里的记忆"同一条理由。
203
- const offNote = row.disabled ? '(注意:整台服务器当前是停用的,这个工具级开关要等它启用才生效)' : '';
203
+ const offNote = row.disabled ? ' (note: the whole server is off right now, so this tool-level switch only takes effect once the server is on)' : '';
204
204
  return 'OK: ' + String(row.serverName) + ' tool ' + tool + ' now ' + (enabled ? 'enabled' : 'disabled') + offNote +
205
- (syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '');
205
+ (syncErr ? '\nWARN: the current scene archive was not synced (' + syncErr + ')' : '');
206
206
  }
207
207
  if (action === 'restart') {
208
208
  const r = await deps.mcpmRestart({ id: row.id, level: row.level });
209
209
  if (!r || r.ok === false)
210
- throw new Error((r && r.error) || '重启失败');
210
+ throw new Error((r && r.error) || 'restart failed');
211
211
  return 'OK: ' + String(row.serverName) + ' (' + row.level + ') restarted';
212
212
  }
213
213
  const r = await deps.mcpmSetEnabled({ id: row.id, level: row.level, enabled });
214
214
  if (!r || r.ok === false)
215
- throw new Error((r && r.error) || '切换服务器启停失败');
215
+ throw new Error((r && r.error) || 'failed to switch this server');
216
216
  // 场景未锁定时,页面 / 模型改的开关都要落进当前场景的档案(与 handlers 层同一套同步)。
217
217
  const syncErr = await deps.syncSwitchToScene('mcpm-set-enabled', { id: row.id, level: row.level, enabled });
218
218
  return 'OK: ' + String(row.serverName) + ' (' + row.level + ') now ' + (enabled ? 'enabled' : 'disabled') +
219
- (syncErr ? '\nWARN: 当前场景档案未同步(' + syncErr + ')' : '');
219
+ (syncErr ? '\nWARN: the current scene archive was not synced (' + syncErr + ')' : '');
220
220
  },
221
221
  }));
222
222
  register(defineTool({
@@ -232,7 +232,7 @@ export function buildMcpTools(deps) {
232
232
  env: { type: 'string', description: 'Extra env vars, key=value per line (stdio). Omit = keep.' },
233
233
  level: { type: 'string', description: 'project or global (project when creating); only needed for same-name servers.' },
234
234
  // 名字与注入段对齐(2026-10-14 对抗性审查 P1-2):注入段与工具输出都写 `user-hint`
235
- // (对应中文那句「用户提示:」),参数名跟着叫 `hint`,一份数据三个面一个词。
235
+ // (对应注入行里那句 `user hint:`),参数名跟着叫 `hint`,一份数据三个面一个词。
236
236
  // 「写什么」那半句已经由注入框架的 `how` 行逐字说过("选服务器之前先看一眼"),
237
237
  // 这里只留参数独有的信息:长度上限与截断行为(2026-10-14 审查 P2-4)。
238
238
  hint: { type: 'string', description: "Short hint for later (≤300 chars; longer is stored whole but truncated in listings). '' clears the existing hint; omit = keep." },
@@ -244,7 +244,7 @@ export function buildMcpTools(deps) {
244
244
  throw new Error(blocked);
245
245
  const server = String((args && args.server) || '').trim();
246
246
  if (!server)
247
- throw new Error('缺少参数:server(服务器名)');
247
+ throw new Error('missing argument: server (the server name)');
248
248
  const levelArg = args && args.level !== undefined ? String(args.level) : undefined;
249
249
  // `hint` 的三态(2026-10-15 修 P1-5):省略 = 保持;`''` = **清空**;非空 = 设置。
250
250
  // 此前用 `hintText.trim() !== ''` 判"要不要写备注",把"清空"与"省略"混成一件事 ——
@@ -255,20 +255,20 @@ export function buildMcpTools(deps) {
255
255
  const clearsNote = hintGiven && hintText.trim() === '';
256
256
  const list = await deps.mcpmListView();
257
257
  if (!list || list.ok === false)
258
- throw new Error((list && list.error) || '读取 MCP 配置失败');
258
+ throw new Error((list && list.error) || 'failed to read the MCP config');
259
259
  const rows = list.rows || [];
260
260
  const hits = rows.filter((r) => String(r.serverName) === server);
261
261
  // ── 建:transport 与它那一侧的必填项都得给 ─────────────────────────────
262
262
  if (!hits.length) {
263
263
  const transport = String((args && args.transport) || '').trim();
264
264
  if (transport !== 'stdio' && transport !== 'streamable-http') {
265
- throw new Error('新建服务器必须给 transport:stdio 或 streamable-http');
265
+ throw new Error('creating a server needs transport: stdio or streamable-http');
266
266
  }
267
267
  if (transport === 'stdio' && String((args && args.command) || '').trim() === '') {
268
- throw new Error('新建 stdio 服务器必须给 command(宿主会按它 spawn 进程)');
268
+ throw new Error('a new stdio server needs command (the host spawns that process)');
269
269
  }
270
270
  if (transport === 'streamable-http' && String((args && args.url) || '').trim() === '') {
271
- throw new Error('新建 streamable-http 服务器必须给 url');
271
+ throw new Error('a new streamable-http server needs url');
272
272
  }
273
273
  const payload = {
274
274
  serverName: server,
@@ -283,14 +283,14 @@ export function buildMcpTools(deps) {
283
283
  payload.level = levelArg;
284
284
  const r = await deps.mcpmAdd(payload);
285
285
  if (!r || r.ok === false)
286
- throw new Error((r && r.error) || '新增服务器失败');
286
+ throw new Error((r && r.error) || 'failed to add the server');
287
287
  // 备注单独走 `mcpm-note`(`mcpm-add` 不认这个字段)。失败**不回滚**主写入:
288
288
  // 服务器已经建好了,"备注没写上"是 warning,不是失败(与 0.13.0 界面同规格)。
289
289
  let warn = '';
290
290
  if (hintGiven && !clearsNote) {
291
291
  const n = await deps.mcpmNote({ id: String(r.row.id), note: hintText });
292
292
  if (!n || n.ok === false)
293
- warn = '\nWARN: 服务器已建好,但备注没写进去(' + ((n && n.error) || '未知原因') + ')';
293
+ warn = '\nWARN: the server was created, but the hint was not written (' + ((n && n.error) || 'unknown reason') + ')';
294
294
  }
295
295
  return 'OK: created ' + r.row.id + ' at ' + r.row.level + warn;
296
296
  }
@@ -298,7 +298,7 @@ export function buildMcpTools(deps) {
298
298
  const cur = locate(hits, server, levelArg, listHint(exec && exec.agent));
299
299
  const transport = args && args.transport !== undefined ? String(args.transport) : String(cur.transport);
300
300
  if (transport !== 'stdio' && transport !== 'streamable-http') {
301
- throw new Error('transport 只能是 stdio 或 streamable-http(这台现在是:' + String(cur.transport) + ')');
301
+ throw new Error('transport must be stdio or streamable-http (this one is: ' + String(cur.transport) + ')');
302
302
  }
303
303
  const touched = [];
304
304
  const ignored = [];
@@ -365,15 +365,15 @@ export function buildMcpTools(deps) {
365
365
  // 直接写备注。这与"三个可改字段一个都没给就拒"是同一条理由的两面。
366
366
  if (!touched.length && !levelChanged) {
367
367
  if (!hintGiven)
368
- throw new Error('没有要改的东西:transport / url / command / args / headers / env / level / hint 至少给一个');
368
+ throw new Error('nothing to change: give at least one of transport / url / command / args / headers / env / level / hint');
369
369
  const n = await deps.mcpmNote({ id: cur.id, note: hintText });
370
370
  if (!n || n.ok === false)
371
- throw new Error((n && n.error) || '写备注失败');
372
- return 'OK: updated ' + String(cur.serverName) + '(' + (clearsNote ? 'hint 已清空' : '改了 hint') + ')';
371
+ throw new Error((n && n.error) || 'failed to write the hint');
372
+ return 'OK: updated ' + String(cur.serverName) + ' (' + (clearsNote ? 'hint cleared' : 'hint changed') + ')';
373
373
  }
374
374
  const r = await deps.mcpmEdit(payload);
375
375
  if (!r || r.ok === false)
376
- throw new Error((r && r.error) || '修改服务器失败');
376
+ throw new Error((r && r.error) || 'failed to update the server');
377
377
  // `hint` 进 `changed` 必须**等写成功之后**(2026-10-15 修 P1-6):此前按意图先算进去,
378
378
  // mcpmNote 失败时回执一边说"改了 hint"、一边 WARN"备注没写进去",模型读不出到底改没改。
379
379
  const changed = touched.concat(levelChanged ? ['level'] : []);
@@ -385,14 +385,14 @@ export function buildMcpTools(deps) {
385
385
  if (hintGiven) {
386
386
  const n = await deps.mcpmNote({ id: cur.id, note: hintText });
387
387
  if (!n || n.ok === false)
388
- warn += '\nWARN: 服务器已保存,但备注没写进去(' + ((n && n.error) || '未知原因') + ')';
388
+ warn += '\nWARN: the server was saved, but the hint was not written (' + ((n && n.error) || 'unknown reason') + ')';
389
389
  else
390
- changed.push(clearsNote ? 'hint(已清空)' : 'hint');
390
+ changed.push(clearsNote ? 'hint (cleared)' : 'hint');
391
391
  }
392
392
  if (ignored.length) {
393
- warn += '\nWARN: ' + ignored.join(' / ') + ' 与当前 transport(' + transport + ')无关,已忽略';
393
+ warn += '\nWARN: ' + ignored.join(' / ') + ' do not apply to the current transport (' + transport + ') and were ignored';
394
394
  }
395
- return 'OK: updated ' + String(cur.serverName) + '(改了 ' + changed.join('、') + ')' + warn;
395
+ return 'OK: updated ' + String(cur.serverName) + ' (changed ' + changed.join(', ') + ')' + warn;
396
396
  },
397
397
  }));
398
398
  }
@@ -26,7 +26,7 @@ export function buildMemoryTools(deps) {
26
26
  name: 'memory_manager_list',
27
27
  // 末尾**不点名** `scene_manager_list`:场景一族在出厂默认里就是关着的,指向一条模型没有的
28
28
  // 工具只会让它去猜名字(用户 2026-09-23 把默认名单扩到十五条时去掉的这句)。
29
- description: 'List memories (id, scene, enabled, form flat|bundle, description) plus the scene injection currently follows. What is injected each turn is in the「当前记忆」reminder; use this for ids and for entries that are off. Pass a `scene` to filter by scene; all=true lists everything.',
29
+ description: 'List memories (id, scene, enabled, form flat|bundle, description) plus the scene injection currently follows. What is injected each turn is in the "Current memories" reminder; use this for ids and for entries that are off. Pass a `scene` to filter by scene; all=true lists everything.',
30
30
  parameters: {
31
31
  // 参数名与其余五个域同词(2026-10-14 对抗性审查 P1-3):同一个概念在工具面只该有一个名字,
32
32
  // 而 `scene` 是它在 `scene_manager_*`、`<scene>/<name>` 与注入段标题里的叫法。
@@ -44,7 +44,7 @@ export function buildMemoryTools(deps) {
44
44
  opArgs.group = args.scene;
45
45
  const r = await deps.rulesOps['rules-list'](opArgs);
46
46
  if (!r || r.ok === false)
47
- throw new Error((r && r.error) || '读取规则失败');
47
+ throw new Error((r && r.error) || 'failed to read memories');
48
48
  // 「会注入」的判定与记忆段同源(renderSceneMemory):global 场景恒常注入,其余场景要
49
49
  // 在 index.active 里,再叠加单条 enabled 与同名 bundle 的 shadowed。默认只列这些,
50
50
  // 全列一次会把没启用的条目也算进上下文(用户裁定 2026-09-16,与技能列表同口径)。
@@ -52,37 +52,37 @@ export function buildMemoryTools(deps) {
52
52
  (group !== '' && (r.activeMode === 'all' || String(r.activeScene || '') === group));
53
53
  const stateOf = (x) => {
54
54
  if (x.shadowed === true)
55
- return { injects: false, label: '被同名覆盖' };
55
+ return { injects: false, label: 'shadowed' };
56
56
  if (x.enabled === false)
57
- return { injects: false, label: '已停用' };
57
+ return { injects: false, label: 'disabled' };
58
58
  if (!sceneOn(String(x.group || '')))
59
- return { injects: false, label: '场景未启用' };
60
- return { injects: true, label: '已启用' };
59
+ return { injects: false, label: 'scene off' };
60
+ return { injects: true, label: 'injects' };
61
61
  };
62
62
  const rows = (r.rules || []).map((x) => ({ x, ...stateOf(x) }));
63
63
  const shown = showAll ? rows : rows.filter((row) => row.injects);
64
- const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || '未归属场景') + '] ' + label +
64
+ const lines = shown.map(({ x, label }) => ('- ' + x.id + ' [' + (x.group || 'no scene') + '] ' + label +
65
65
  // 形态与附件(2026-10-15):此前不报,模型分不清哪条是 bundle、更不知道它带了几个附件
66
66
  // (`rules-list` 早就把 `form` 与 bundle 的附件摘要给了出来,只是这里没渲染)。
67
67
  (x.form === 'bundle'
68
- ? ' · bundle' + (x.attach && x.attach.count ? '(' + x.attach.count + ' 个附件)' : '')
68
+ ? ' · bundle' + (x.attach && x.attach.count ? ' (' + x.attach.count + ' attachment(s))' : '')
69
69
  : '') +
70
70
  (x.description ? ' — ' + x.description : '')));
71
71
  // 注入按哪个场景走 —— 上面那句「场景未启用」只有配上它才可解读:少了这一行,
72
72
  // 模型看到一片"未启用"会以为是条目自己的问题。(场景本身的清单在 `scene_manager_list`,
73
73
  // 2026-09-23 拆过去了;这里只留"记忆按哪一集注入",因为那是记忆域自己的现状。)
74
74
  const scope = r.activeMode === 'all'
75
- ? '全部场景(历史默认,未收窄)'
76
- : (r.activeScene ? '「' + String(r.activeScene) + '」' : '未启用任何场景');
77
- const header = '记忆:' + (showAll
78
- ? rows.length + ' 条'
79
- : shown.length + ' 条会注入 / 共 ' + rows.length + ' 条' +
80
- (shown.length === rows.length ? '' : '(传 all=true 看全部)')) +
81
- '\n注入范围:' + scope + ' + global(global 恒常注入)\n';
75
+ ? 'every scene (historical default, not narrowed)'
76
+ : (r.activeScene ? '"' + String(r.activeScene) + '"' : 'no scene is on');
77
+ const header = 'memories: ' + (showAll
78
+ ? rows.length + ' total'
79
+ : shown.length + ' injecting / ' + rows.length + ' total' +
80
+ (shown.length === rows.length ? '' : ' (pass all=true for all)')) +
81
+ '\ninjects from: ' + scope + ' + global (global always injects)\n';
82
82
  // 注入边界:压制型预设(persona complete / 关闭运行时上下文)下本插件默认不注入,
83
83
  // 此时列出的记忆**不在**模型上下文里。必须说出来,否则模型会假设自己已经看到正文。
84
84
  const notice = await deps.reachNoticeForAgent(deps.presetRoster(), exec && exec.agent && exec.agent.ctx, deps.injectNoticeOptions());
85
- return header + (lines.join('\n') || '(无记忆)') + notice;
85
+ return header + (lines.join('\n') || '(none)') + notice;
86
86
  },
87
87
  }));
88
88
  register(defineTool({
@@ -99,7 +99,7 @@ export function buildMemoryTools(deps) {
99
99
  // op 的字段名是 `id`,不改 op:这里显式转换(键名对模型是 `<场景>/<名字>`,对 op 是 `id`)。
100
100
  const r = await deps.rulesOps['rules-read']({ id: String((args && args.memory) || '') });
101
101
  if (!r || r.ok === false)
102
- throw new Error((r && r.error) || '读取规则失败');
102
+ throw new Error((r && r.error) || 'failed to read memories');
103
103
  const rule = r.rule || {};
104
104
  // 形态与附件名(2026-10-15):op 一直把 `attachments` 给了出来,这里此前**整个丢掉**,
105
105
  // 于是模型连"这条记忆是 bundle、带了 3 个附件"都看不到。只给名字不给内容 ——
@@ -107,9 +107,9 @@ export function buildMemoryTools(deps) {
107
107
  const attach = Array.isArray(rule.attachments) ? rule.attachments : [];
108
108
  const form = rule.form === 'bundle' ? 'bundle' : 'flat';
109
109
  const attLine = attach.length
110
- ? '\n附件(' + attach.length + ' 个):' + attach.map((a) => String((a && a.name) || a)).join('、')
110
+ ? '\nattachments (' + attach.length + '): ' + attach.map((a) => String((a && a.name) || a)).join(', ')
111
111
  : '';
112
- return '# ' + rule.id + '(' + form + ')' + attLine + '\n\n' + (rule.body || '');
112
+ return '# ' + rule.id + ' (' + form + ')' + attLine + '\n\n' + (rule.body || '');
113
113
  },
114
114
  }));
115
115
  register(defineTool({
@@ -143,10 +143,10 @@ export function buildMemoryTools(deps) {
143
143
  // `enabled` 必须是布尔:`rules-toggle` 缺了它直接拒(ops/memory.ts),但那是给用户看的
144
144
  // 中文 op 错;这里先挡一句说清"这条工具要什么"。
145
145
  if (typeof (args && args.enabled) !== 'boolean')
146
- throw new Error('缺少参数:enabled 必须是 true 或 false');
146
+ throw new Error('missing argument: enabled must be true or false');
147
147
  const r = await deps.rulesOps['rules-toggle']({ id: memory, enabled: args.enabled });
148
148
  if (!r || r.ok === false)
149
- throw new Error((r && r.error) || '切换记忆启停失败');
149
+ throw new Error((r && r.error) || 'failed to switch this memory');
150
150
  const rule = r.rule || {};
151
151
  // 「启用了一条不在启用场景里的记忆」是一次静默无效:单条开关拨上去了,注入集里却没有它。
152
152
  // 这一句不说,模型会以为已经生效 —— 与 skill_manager_switch 说清影子副本同一条理由。
@@ -157,7 +157,7 @@ export function buildMemoryTools(deps) {
157
157
  const group = String(rule.group || '');
158
158
  const on = group === 'global' || (group !== '' && (all.activeMode === 'all' || String(all.activeScene || '') === group));
159
159
  if (!on)
160
- sceneNote = ' — BUT its scene「' + group + '」is not the active one, so it still will not be injected; enable that scene (or move it to global) first';
160
+ sceneNote = ' — BUT its scene "' + group + '" is not the one injecting, so it still will not be injected; turn that scene on (or move it to global) first';
161
161
  }
162
162
  }
163
163
  catch {
@@ -193,7 +193,7 @@ export function buildMemoryTools(deps) {
193
193
  // 清单里打印的就是 `<场景>/<名字>`,照抄比拼装准(§2 规则一)。
194
194
  const slash = memory.indexOf('/');
195
195
  if (slash <= 0 || slash === memory.length - 1) {
196
- throw new Error('memory 必须是 <场景>/<名字> 两段(照抄清单里那一行;全局场景写 global/<名字>):' + (memory || '(空)'));
196
+ throw new Error('memory must be <scene>/<name> (copy that from the listing; the always-on scene is written global/<name>): ' + (memory || '(empty)'));
197
197
  }
198
198
  const group = memory.slice(0, slash);
199
199
  const name = memory.slice(slash + 1);
@@ -201,7 +201,7 @@ export function buildMemoryTools(deps) {
201
201
  // 这一侧 —— `rules-create` 对同名是**拒绝**的(`error.rules.exists`),op 不会 upsert。
202
202
  const all = await deps.rulesOps['rules-list']({});
203
203
  if (!all || all.ok === false)
204
- throw new Error((all && all.error) || '读取规则失败');
204
+ throw new Error((all && all.error) || 'failed to read memories');
205
205
  const hit = (all.rules || []).find((x) => String(x.id) === memory);
206
206
  if (hit) {
207
207
  // 改的分支一律 read-modify-write(省略 = 保持),因为 `rules-update` 是整份重写。
@@ -209,7 +209,7 @@ export function buildMemoryTools(deps) {
209
209
  // 模型以为改了点什么,其实只是把文件重写了一遍。
210
210
  const touched = ['body', 'description', 'nextName'].filter((k) => args[k] !== undefined);
211
211
  if (!touched.length)
212
- throw new Error('没有要改的东西:body / description / nextName 至少给一个');
212
+ throw new Error('nothing to change: give at least one of body / description / nextName');
213
213
  const payload = { id: memory };
214
214
  // 只带模型真给过的键:`description` 传空串在 op 里是"删除并重新派生",与"未给"是两种
215
215
  // 意图,都必须原样透传 —— 所以用 `!== undefined` 而不是真值判断。
@@ -221,10 +221,10 @@ export function buildMemoryTools(deps) {
221
221
  payload.name = args.nextName;
222
222
  const r = await deps.rulesOps['rules-update'](payload);
223
223
  if (!r || r.ok === false)
224
- throw new Error((r && r.error) || '更新记忆失败');
224
+ throw new Error((r && r.error) || 'failed to update the memory');
225
225
  const rule = r.rule || {};
226
- return 'OK: updated ' + String(rule.id || memory) + '(改了 ' + touched.join('、') + ')' +
227
- (rule.id && String(rule.id) !== memory ? ' → 现在是 ' + rule.id : '');
226
+ return 'OK: updated ' + String(rule.id || memory) + ' (changed ' + touched.join(', ') + ')' +
227
+ (rule.id && String(rule.id) !== memory ? ' → now ' + rule.id : '');
228
228
  }
229
229
  // 建的分支:正文与描述都必填。`rules-create` 自己会拒空正文与非法名(ops/memory.ts),
230
230
  // 但那是给用户看的中文 op 错;模型需要的是"这条工具要什么",所以在工具侧先挡、点名缺哪个。
@@ -235,7 +235,7 @@ export function buildMemoryTools(deps) {
235
235
  description.trim() === '' ? 'description' : '',
236
236
  ].filter((x) => x !== '');
237
237
  if (missing.length)
238
- throw new Error('新建记忆必须给 ' + missing.join(' 与 ') + '(' + memory + ' 还不存在)');
238
+ throw new Error('creating a memory needs ' + missing.join(' and ') + ' (' + memory + ' does not exist yet)');
239
239
  // 查重只**说出来**,不拒绝 —— 猜错一次就把该记的东西记不下去,比多记一条代价大得多;
240
240
  // 而且"像不像同一条"本来就是人判断的事。只在建的分支提示:改的分支没有"再记一条"的风险。
241
241
  const norm = (value) => String(value ?? '').toLowerCase().replace(/\s+/g, ' ').trim();
@@ -256,11 +256,11 @@ export function buildMemoryTools(deps) {
256
256
  .slice(0, 3);
257
257
  const r = await deps.rulesOps['rules-create']({ group, name, description, body });
258
258
  if (!r || r.ok === false)
259
- throw new Error((r && r.error) || '创建规则失败');
259
+ throw new Error((r && r.error) || 'failed to create the memory');
260
260
  // 刚建好的这条自己会在列表里,从名单里去掉。
261
261
  const dup = similar.filter((id) => id !== String(r.rule.id));
262
262
  const warn = dup.length
263
- ? `\n注意:已有 ${dup.length} 条名字或描述与它很像(${dup.join('、')})—— 若是同一件事,改那一条而不是留着两条。`
263
+ ? `\nnote: ${dup.length} existing memories have a similar name or description (${dup.join(', ')}) — if it is the same thing, update that one instead of keeping two.`
264
264
  : '';
265
265
  // 回执必须同时说清**两个**条件(2026-10-15 smoke-test P1-3):新建的记忆在 op 层是
266
266
  // `enabled: false`(ops/memory.ts 的 rulesCreate),而原先这句只提场景 —— 读起来像
@@ -268,10 +268,10 @@ export function buildMemoryTools(deps) {
268
268
  // 的 enableHint 同一形状:点名的工具在出厂默认名单里是关着的,关着就改说"用户开启后"。
269
269
  const sceneActive = group === 'global' ||
270
270
  (group !== '' && (all.activeMode === 'all' || String(all.activeScene || '') === group));
271
- const sceneTail = sceneActive ? '' : ',并启用场景「' + group + '」';
271
+ const sceneTail = sceneActive ? '' : ', and turn scene "' + group + '" on';
272
272
  const enableHint = deps.toolVisible('memory_manager_switch')
273
- ? '(新建默认停用,要生效先 memory_manager_switch 开它' + sceneTail + ')'
274
- : '(新建默认停用,用户开启后才能生效' + sceneTail + ')';
273
+ ? ' (created disabled — turn it on first with memory_manager_switch' + sceneTail + ')'
274
+ : ' (created disabled — the user has to turn it on first' + sceneTail + ')';
275
275
  return 'OK: created ' + r.rule.id + enableHint + warn;
276
276
  },
277
277
  }));