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
@@ -19,7 +19,7 @@ export function buildPromptTools(deps) {
19
19
  // 那句 `(that reminder is absent when the preset carries the official instructions line)`
20
20
  // 删掉(2026-10-15 注入文本审查):模型**观测不到**"official instructions line"这个条件,
21
21
  // 拿它做不了任何决定 —— reminder 在不在,它抬头就能看见,不必由描述来预告。
22
- description: 'List AGENTS.md presets (id, description, active state, file path); read that file for a preset body. The one in effect is in the「当前提示词」reminder, whose「来源:」line names the file. Defaults to the one in effect; all=true for the whole library.',
22
+ description: 'List AGENTS.md presets (id, description, active state, file path); read that file for a preset body. The one in effect is in the "Current prompt" reminder, whose "Instructions from:" line names the file. Defaults to the one in effect; all=true for the whole library.',
23
23
  parameters: {
24
24
  all: { type: 'boolean', description: 'Include inactive presets (default false).' },
25
25
  },
@@ -28,7 +28,7 @@ export function buildPromptTools(deps) {
28
28
  const showAll = Boolean(args && args.all === true);
29
29
  const r = await deps.promptOps['agentsmd-list']({});
30
30
  if (!r || r.ok === false)
31
- throw new Error((r && r.error) || '读取提示词预设失败');
31
+ throw new Error((r && r.error) || 'failed to read the AGENTS.md presets');
32
32
  const all = r.presets || [];
33
33
  // 默认只列「这份 AGENTS.md 来自哪个预设」(用户裁定 2026-09-16)。
34
34
  // 判定分两级:`active`(场景驱动时 = 场景绑的那份;否则 = 内容逐字节相同)优先,
@@ -88,21 +88,21 @@ export function buildPromptTools(deps) {
88
88
  // 不说的话用户以为 AGENTS.md 还在(0.19.1 界面弹窗报的就是这两种)。
89
89
  const r = await deps.unapplyPreset();
90
90
  if (!r.ok)
91
- throw new Error(r.error || '取消应用失败');
91
+ throw new Error(r.error || 'failed to cancel the application');
92
92
  return r.removed === true
93
93
  ? 'OK: application cancelled — ~/.dsh/AGENTS.md did not exist before the run started, so the file was removed (effective next turn).'
94
94
  : 'OK: application cancelled — ~/.dsh/AGENTS.md is back to what it was before the run started (effective next turn).';
95
95
  }
96
96
  const id = String((args && args.id) || '').trim();
97
97
  if (id === '')
98
- throw new Error('缺少参数:id(只有 apply=false 取消时才不用给)');
98
+ throw new Error('missing argument: id (only optional when apply=false cancels)');
99
99
  const r = await deps.applyPresetGuarded(id);
100
100
  if (!r.ok)
101
101
  throw new Error(r.error);
102
102
  // 场景驱动时写的是「场景绑定」,回执必须说清改的是哪个场景 —— 只说「已应用到
103
103
  // AGENTS.md」会让模型对用户谎报(用户不知道自己的场景绑定被换掉了)。
104
104
  if (r.viaScene) {
105
- return 'OK: preset ' + id + ' is now the prompt binding of scene 「' + (r.scene || '') + '」 and the scene was re-synced (effective next turn; ~/.dsh/AGENTS.md now carries that scene\'s binding).';
105
+ return 'OK: preset ' + id + ' is now the prompt binding of scene "' + (r.scene || '') + '" and the scene was re-synced (effective next turn; ~/.dsh/AGENTS.md now carries that scene\'s binding).';
106
106
  }
107
107
  // 生效时机与宿主行为对齐(index.ts 提示词服务一节考证过):dsh-agent-instructions
108
108
  // 每个 pre-step 都 stat 比对版本,写文件即下一轮重读 —— 不是"下个会话"。说错了
@@ -50,13 +50,13 @@ function restoredName(r) {
50
50
  async function runRemove(deps, cfg, args) {
51
51
  const action = String((args && args.action) || '');
52
52
  if (action !== 'trash' && action !== 'restore' && action !== 'list') {
53
- throw new Error('action 只能是 trash / restore / list(收到:' + (action || '(空)') + ')');
53
+ throw new Error('action must be trash / restore / list (got: ' + (action || '(empty)') + ')');
54
54
  }
55
55
  if (action === 'list') {
56
56
  const entries = await cfg.list();
57
57
  return entries.length
58
- ? '回收站(' + entries.length + ' 条):\n' + entries.map((e) => '- ' + e.id + ' · ' + e.name + (e.deletedAt ? ' · ' + e.deletedAt : '')).join('\n')
59
- : '回收站是空的。';
58
+ ? 'trash: ' + entries.length + ' item(s)\n' + entries.map((e) => '- ' + e.id + ' · ' + e.name + (e.deletedAt ? ' · ' + e.deletedAt : '')).join('\n')
59
+ : 'the trash is empty.';
60
60
  }
61
61
  // 进 / 出回收站都是写:过场景冻结守卫(与面板的删除按钮同一档)。
62
62
  const blocked = await deps.lockedSceneGuard();
@@ -64,19 +64,19 @@ async function runRemove(deps, cfg, args) {
64
64
  throw new Error(blocked);
65
65
  const id = String((args && args.id) || '').trim();
66
66
  if (id === '')
67
- throw new Error('缺少参数:id(action=' + action + ' 需要)');
67
+ throw new Error('missing argument: id (required by action=' + action + ')');
68
68
  const r = action === 'trash' ? await cfg.trash(args) : await cfg.restore(args);
69
69
  if (!r || r.ok === false)
70
- throw new Error((r && r.error) || (action === 'trash' ? '移入回收站失败' : '恢复失败'));
70
+ throw new Error((r && r.error) || (action === 'trash' ? 'failed to move it to the trash' : 'restore failed'));
71
71
  if (action === 'trash') {
72
- return 'OK: ' + cfg.label + '「' + id + '」已移入回收站(可恢复;永久删除在面板的回收站里)';
72
+ return 'OK: ' + cfg.label + ' "' + id + '" moved to the trash (recoverable; permanent deletion lives in the panel trash)';
73
73
  }
74
74
  // 恢复的回执要说**恢复成了什么**,而不是回收站条目 id(2026-10-15 smoke-test P1-6):
75
75
  // `id` 是 `action=list` 给的那一串(记忆 `mv…`、技能 `<时间戳>-<uuid>`),模型看到它
76
76
  // 认不出恢复了哪一条;退号恢复时最终落盘的名字还与回收站里的原名不同,更要说准。
77
77
  const name = restoredName(r) || id;
78
- return 'OK: 已从回收站恢复 ' + cfg.label + '「' + name + '」' +
79
- (name === id ? '' : '(回收站条目 ' + id + ')');
78
+ return 'OK: restored ' + cfg.label + ' "' + name + '" from the trash' +
79
+ (name === id ? '' : ' (trash entry ' + id + ')');
80
80
  }
81
81
  export function buildRemoveTools(deps) {
82
82
  const { defineTool, register } = deps;
@@ -86,18 +86,18 @@ export function buildRemoveTools(deps) {
86
86
  const pr = deps.promptOps;
87
87
  const sc = deps.sceneOps;
88
88
  const memoryCfg = {
89
- noun: 'memory', label: '记忆',
89
+ noun: 'memory', label: 'memory',
90
90
  trash: (a) => mem['rules-remove']({ id: String(a.id || '') }),
91
91
  restore: (a) => mem['rules-restore']({ trashId: String(a.id || '') }),
92
92
  list: async () => {
93
93
  const r = await mem['rules-trash-list']({});
94
94
  if (!r || r.ok === false)
95
- throw new Error((r && r.error) || '读取回收站失败');
95
+ throw new Error((r && r.error) || 'failed to read the trash');
96
96
  return (r.entries || []).map((e) => ({ id: String(e.trashId), name: String(e.name), deletedAt: e.deletedAt }));
97
97
  },
98
98
  };
99
99
  const skillCfg = {
100
- noun: 'skill', label: '技能',
100
+ noun: 'skill', label: 'skill',
101
101
  // 与 `skill_manager_switch` 同一条(smoke-test P0-1):不给 root 就按名字在全部来源里
102
102
  // 找胜出的那一份。默认值原本是 dsh,而 `skill_manager_save` 落 hub —— 「建完就删」
103
103
  // 必然报「技能不存在」。删一个真的不存在的技能时退回 dsh,交给 op 报它自己的错。
@@ -117,30 +117,30 @@ export function buildRemoveTools(deps) {
117
117
  },
118
118
  };
119
119
  const subagentCfg = {
120
- noun: 'persona', label: '人设',
120
+ noun: 'persona', label: 'persona',
121
121
  trash: (a) => sub['subagent-delete']({ name: String(a.id || '') }),
122
122
  restore: (a) => sub['subagent-trash-restore']({ id: String(a.id || '') }),
123
123
  list: async () => {
124
124
  const r = await sub['subagent-trash-list']({});
125
125
  if (!r || r.ok === false)
126
- throw new Error((r && r.error) || '读取回收站失败');
126
+ throw new Error((r && r.error) || 'failed to read the trash');
127
127
  return (r.trash || []).map((e) => ({ id: String(e.id), name: String(e.name), deletedAt: e.deletedAt }));
128
128
  },
129
129
  };
130
130
  const promptCfg = {
131
- noun: 'preset', label: '提示词预设',
131
+ noun: 'preset', label: 'preset',
132
132
  trash: (a) => pr['agentsmd-remove']({ id: String(a.id || '') }),
133
133
  restore: (a) => pr['agentsmd-trash-restore']({ id: String(a.id || '') }),
134
134
  list: async () => {
135
135
  const r = await pr['agentsmd-trash-list']({});
136
136
  if (!r || r.ok === false)
137
- throw new Error((r && r.error) || '读取回收站失败');
137
+ throw new Error((r && r.error) || 'failed to read the trash');
138
138
  const arr = Array.isArray(r) ? r : (r.presets || r.trash || r.entries || []);
139
139
  return arr.map((e) => ({ id: String(e.id), name: String(e.name || e.id), deletedAt: e.deletedAt }));
140
140
  },
141
141
  };
142
142
  const sceneCfg = {
143
- noun: 'scene', label: '场景',
143
+ noun: 'scene', label: 'scene',
144
144
  // 删场景走 handlers 里那份(`withAgentsMdSync` 包过):直调 `memoriesService.ops` 会漏掉
145
145
  // 「场景绑的预设曾写进 AGENTS.md,删了要跟着处理」那一步。
146
146
  trash: (a) => deps.sceneRemove({ name: String(a.id || '') }),
@@ -148,7 +148,7 @@ export function buildRemoveTools(deps) {
148
148
  list: async () => {
149
149
  const r = await sc['scene-trash-list']({});
150
150
  if (!r || r.ok === false)
151
- throw new Error((r && r.error) || '读取回收站失败');
151
+ throw new Error((r && r.error) || 'failed to read the trash');
152
152
  return (r.trash || []).map((e) => ({ id: String(e.id), name: String(e.name), deletedAt: e.deletedAt }));
153
153
  },
154
154
  };
@@ -60,7 +60,7 @@ function toolTableLabel(name) {
60
60
  const v = typeof name === 'string' ? name.trim() : '';
61
61
  if (!v)
62
62
  return '';
63
- return v === FACTORY_TOOL_TABLE_PRESET ? '出厂默认' : v;
63
+ return v === FACTORY_TOOL_TABLE_PRESET ? 'factory default' : v;
64
64
  }
65
65
  /**
66
66
  * 一个档案的名单渲染(`_list` 用)。**点名而不是只给数量**:模型要判断"这个场景进入时会不会
@@ -68,23 +68,23 @@ function toolTableLabel(name) {
68
68
  */
69
69
  function archiveText(archive) {
70
70
  if (!archive || typeof archive !== 'object')
71
- return '未建(进入时 MCP / 技能 / 人设 三个域全部停用)';
71
+ return 'not created (entering turns off all MCP / skill / persona domains)';
72
72
  const keys = (v) => (v && typeof v === 'object' && !Array.isArray(v) ? Object.keys(v) : []);
73
73
  const list = (v) => (Array.isArray(v) ? v.map((x) => String(x)) : []);
74
74
  const parts = [
75
75
  'MCP ' + toolNameList(keys(archive.mcp)),
76
- '技能 ' + toolNameList(list(archive.skills)),
77
- '人设 ' + toolNameList(list(archive.subagents)),
76
+ 'skills ' + toolNameList(list(archive.skills)),
77
+ 'personas ' + toolNameList(list(archive.subagents)),
78
78
  ];
79
79
  const notes = keys(archive.mcpNotes);
80
80
  if (notes.length)
81
- parts.push('备注 ' + toolNameList(notes));
81
+ parts.push('notes ' + toolNameList(notes));
82
82
  // 模型工具表方案(0.15.0 起档案里有这一栏):进入时换的是**模型自己能用什么**,不报就是
83
83
  // 让它事后发现自己少了工具却说不出原因。只给方案名 —— 关停名单不在档案里,硬算要多读一次
84
84
  // 方案清单,而真正需要条数的时机是切换之后(那条回执有)。没绑就不冒这一格(与备注同口径)。
85
85
  const tt = toolTableLabel(archive.toolTablePreset);
86
86
  if (tt)
87
- parts.push('工具表方案「' + tt + '」');
87
+ parts.push('tool-table preset "' + tt + '"');
88
88
  // 快捷提示词绑定(0.20.0):进了这个场景会动到 `/` 菜单里哪些条目,不报就只能靠猜。
89
89
  // 空表是"进入后全部停用",与"没绑"(这一域不碰)是两句相反的话,不能压成一个 "0 条"。
90
90
  //
@@ -94,7 +94,7 @@ function archiveText(archive) {
94
94
  // 与 `skills` / `subagents` 两段同款(那两段本来就是 `toolNameList` 印名字)。
95
95
  const qp = archive.quickPrompts;
96
96
  if (Array.isArray(qp))
97
- parts.push(qp.length ? '快捷提示词 ' + toolNameList(qp) : '快捷提示词:进入后全部停用');
97
+ parts.push(qp.length ? 'quick prompts ' + toolNameList(qp) : 'quick prompts: all off after entering');
98
98
  return parts.join(' | ');
99
99
  }
100
100
  /**
@@ -115,9 +115,9 @@ function segmentDiff(key, beforeValue, afterValue) {
115
115
  if (dropped.length)
116
116
  marks.push('-' + toolNameList(dropped));
117
117
  if (!marks.length && JSON.stringify(beforeValue ?? null) !== JSON.stringify(afterValue ?? null))
118
- marks.push('勾选范围有调整');
118
+ marks.push('the checked set changed');
119
119
  return {
120
- text: key + ' ' + b.length + ' 项 → ' + a.length + ' 项' + (marks.length ? '(' + marks.join(' ') + ')' : ''),
120
+ text: key + ' ' + b.length + ' -> ' + a.length + ' items' + (marks.length ? ' (' + marks.join(' ') + ')' : ''),
121
121
  dropped,
122
122
  };
123
123
  }
@@ -133,11 +133,11 @@ function agentsMdNote(sync) {
133
133
  if (!sync || typeof sync !== 'object')
134
134
  return '';
135
135
  if (sync.error)
136
- return '\nWARN: 全局基线(AGENTS.md)未同步:' + String(sync.error);
136
+ return '\nWARN: the global baseline (AGENTS.md) was not synced: ' + String(sync.error);
137
137
  if (sync.applied)
138
- return '\nAGENTS.md ← 场景绑定的预设「' + String(sync.applied) + '」';
138
+ return '\nAGENTS.md <- the scene-bound preset "' + String(sync.applied) + '"';
139
139
  if (sync.restored === true)
140
- return '\nAGENTS.md ← 已恢复进场景前的基线';
140
+ return '\nAGENTS.md <- restored the pre-scene baseline';
141
141
  return '';
142
142
  }
143
143
  export function buildSceneTools(deps) {
@@ -148,15 +148,15 @@ export function buildSceneTools(deps) {
148
148
  // 这种筛选(未启用的场景照样要能被进入,藏着只会让模型以为它不存在)。
149
149
  // 描述与参数说明里**不点名同族工具**(scene_manager_list / _switch / _save 三条都在出厂
150
150
  // 默认的隐藏名单里):静态文本没法按 toolVisible 改写,点名一条模型没有的工具只会让它
151
- // 白跑一趟。要说"去哪看"就用中性说法(the scene listing / the「当前场景」reminder
151
+ // 白跑一趟。要说"去哪看"就用中性说法(the scene listing / the "Current scene" reminder
152
152
  // —— 后者是常驻源,只要场景域开着就在)。点名只留在**运行时回执**里、按可见性分叉。
153
- description: 'List every scene with its archive (what entering it switches on: MCP servers, skills, personas, quick prompts, and the bound model tool-table scheme when there is one), bound prompt preset, memory count and enabled / entered / locked state. The scene currently entered is also shown in the「当前场景」reminder.',
153
+ description: 'List every scene with its archive (what entering it switches on: MCP servers, skills, personas, quick prompts, and the bound model tool-table scheme when there is one), bound prompt preset, memory count and enabled / entered / locked state. The scene currently entered is also shown in the "Current scene" reminder.',
154
154
  parameters: {},
155
155
  output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
156
156
  async execute() {
157
157
  const r = await deps.rulesOps['rules-list']({});
158
158
  if (!r || r.ok === false)
159
- throw new Error((r && r.error) || '读取场景失败');
159
+ throw new Error((r && r.error) || 'failed to read scenes');
160
160
  // 档案与「已进入哪个场景」要额外读一次(纯读盘,`scene-mode-get`)。
161
161
  // 读不到**不阻断**清单:场景行退化成只有名字与启用状态,而不是让整条工具失败 ——
162
162
  // 一份少了档案的清单仍然有用,一次失败什么都没有。
@@ -180,28 +180,28 @@ export function buildSceneTools(deps) {
180
180
  const scenes = r.scenes || [];
181
181
  const lines = scenes.map((s) => {
182
182
  const name = String(s.name);
183
- const state = [s.active === true ? '启用' : '未启用'];
183
+ const state = [s.active === true ? 'on' : 'off'];
184
184
  // 「启用」与「进入」是两个独立状态轴(见 README「进/出模式」),只报前者的话,
185
185
  // 模型会把"启用着"读成"现在就是它" —— 而记忆与提示词只按**进入**的那一个注入。
186
186
  if (entered !== null && name === entered)
187
- state.push('已进入');
187
+ state.push('entered');
188
188
  if (s.locked === true)
189
- state.push('已锁定');
189
+ state.push('locked');
190
190
  // 打**场景名**而不是 label:`scene_manager_save` / `_switch` 要的都是名字(单个
191
191
  // 路径段),label 只是界面显示名。两者不同时把 label 放进括号,别让模型照抄错那个。
192
- const title = name + (s.label && String(s.label) !== name ? '(' + String(s.label) + ')' : '');
193
- const bits = ['- ' + title + ' [' + state.join('·') + '] · 记忆 ' + Number(s.count || 0) + ' 条'];
192
+ const title = name + (s.label && String(s.label) !== name ? ' (' + String(s.label) + ')' : '');
193
+ const bits = ['- ' + title + ' [' + state.join(', ') + '] · memories: ' + Number(s.count || 0)];
194
194
  if (s.description)
195
- bits.push('说明:' + String(s.description));
195
+ bits.push('description: ' + String(s.description));
196
196
  if (s.prompt)
197
- bits.push('预设:' + String(s.prompt));
198
- bits.push('档案:' + (archives === null ? '读不到(不代表没建)' : archiveText(archives[name])));
197
+ bits.push('preset: ' + String(s.prompt));
198
+ bits.push('archive: ' + (archives === null ? 'unreadable (does not mean absent)' : archiveText(archives[name])));
199
199
  return bits.join(' · ');
200
200
  });
201
- return '场景(' + scenes.length + ' 个 · ' +
202
- (r.activeMode === 'all' ? '启用集为历史默认(全部启用)' : '启用集已收窄为单选') +
203
- (entered === null ? ';当前没有进入任何场景' : ';当前已进入「' + entered + '」') + '):\n' +
204
- (lines.join('\n') || '(无场景)');
201
+ return 'scenes (' + scenes.length + ' · ' +
202
+ (r.activeMode === 'all' ? 'the enabled set is the historical default (all enabled)' : 'the enabled set is narrowed to a single scene') +
203
+ (entered === null ? '; no scene is entered right now' : '; currently entered: "' + entered + '"') + '):\n' +
204
+ (lines.join('\n') || '(no scenes)');
205
205
  },
206
206
  }));
207
207
  register(defineTool({
@@ -212,13 +212,13 @@ export function buildSceneTools(deps) {
212
212
  description: 'Enter a scene, or leave the one currently entered. Entering switches on its archive and everything else off, narrows injection to its memories, and applies its bound prompt preset to AGENTS.md. Leaving restores the runtime snapshot taken on entry. Only when the user asks or approves.',
213
213
  parameters: {
214
214
  action: { type: 'string', required: true, enum: ['enter', 'exit'], description: 'enter = switch to the scene named in `scene`; exit = leave the current one (no scene needed).' },
215
- scene: { type: 'string', description: 'Scene name (one path segment), required for enter. Use the exact name, not the display label — the scene listing and the「当前场景」reminder both print names.' },
215
+ scene: { type: 'string', description: 'Scene name (one path segment), required for enter. Use the exact name, not the display label — the scene listing and the "Current scene" reminder both print names.' },
216
216
  },
217
217
  output: { schema: { type: 'string' }, render: (_a, v) => text(v) },
218
218
  async execute(args) {
219
219
  const action = String((args && args.action) || '');
220
220
  if (action !== 'enter' && action !== 'exit') {
221
- throw new Error('action 只能是 enter / exit(收到:' + (action || '(空)') + ')');
221
+ throw new Error('action must be enter / exit (got: ' + (action || '(empty)') + ')');
222
222
  }
223
223
  // 刻意**不过** `lockedSceneGuard()`:场景**自身**的启停不在冻结清单里(冻结的是五个
224
224
  // 域的内容)。锁定中的场景正被进入时不能退出 —— 那条守卫在 `scene-mode-set` 的包装里
@@ -227,10 +227,10 @@ export function buildSceneTools(deps) {
227
227
  if (action === 'enter') {
228
228
  scene = String((args && args.scene) || '').trim();
229
229
  if (scene === '')
230
- throw new Error('action=enter 需要 scene:场景名(照抄 scene_manager_list 里那一行)');
230
+ throw new Error('action=enter needs scene: the scene name (copy it from the scene_manager_list line)');
231
231
  // 场景名是**单个路径段**(与 scene_manager_save 同一条校验、同一句报错)。
232
232
  if (scene.indexOf('/') >= 0 || scene.indexOf('\\') >= 0) {
233
- throw new Error('场景名是单个路径段,不能含 / 或 \\:' + scene);
233
+ throw new Error('a scene name is a single path segment (no / or \\): ' + scene);
234
234
  }
235
235
  }
236
236
  // 先读一次"现在在哪个场景":用来在回执里说清是"从 A 换到 B"还是"本来就在 B"。
@@ -247,14 +247,14 @@ export function buildSceneTools(deps) {
247
247
  // `rules-set-active` 写。少调后者,记忆与提示词仍按旧场景注入 —— 而运行时看起来已经切了。
248
248
  const mode = await deps.archiveOps['scene-mode-set']({ scene: action === 'enter' ? scene : null });
249
249
  if (!mode || mode.ok === false)
250
- throw new Error((mode && mode.error) || (action === 'enter' ? '进入场景失败:' + scene : '退出场景失败'));
250
+ throw new Error((mode && mode.error) || (action === 'enter' ? 'failed to enter the scene: ' + scene : 'failed to exit the scene'));
251
251
  const act = await deps.sceneActivate({ scenes: action === 'enter' ? [scene] : [] });
252
252
  if (!act || act.ok === false) {
253
253
  // 半成品状态:运行时已经切了,启用集合没跟上。**必须说出来** —— 不说的话模型以为
254
254
  // 切完了,而记忆与提示词还是旧场景的。重试一次是安全的(两个 op 都幂等)。
255
- throw new Error((action === 'enter' ? '场景「' + scene + '」的运行时已切换' : '运行时已退出场景') +
256
- ',但启用集合未写入(记忆与提示词仍按旧场景注入):' + ((act && act.error) || '未知错误') +
257
- ' —— 重试一次 scene_manager_switch 即可(两个 op 都幂等)');
255
+ throw new Error((action === 'enter' ? 'runtime switched to scene "' + scene + '"' : 'runtime left the scene') +
256
+ ', but the enabled set was not written (memories and prompts still inject per the old scene): ' + ((act && act.error) || 'unknown error') +
257
+ ' — retry scene_manager_switch once (both ops are idempotent)');
258
258
  }
259
259
  // 回执:说清"从哪到哪 + 实际切了几台 / 几个来源 + 工具表换到哪一份 + 有没有键被丢弃 + AGENTS.md 那一步"。
260
260
  // 都是界面会如实报的同一批事实(`memory.result.modeSet` / `modeSwitched` / `archive.stale`),
@@ -268,10 +268,10 @@ export function buildSceneTools(deps) {
268
268
  ? (mode.applied === null || mode.applied === undefined)
269
269
  : !mode.restored;
270
270
  const head = noop
271
- ? (action === 'enter' ? 'OK: already in scene「' + scene + '」' : 'OK: not in any scene (nothing to leave)')
271
+ ? (action === 'enter' ? 'OK: already in scene "' + scene + '"' : 'OK: not in any scene (nothing to leave)')
272
272
  : action === 'enter'
273
- ? 'OK: ' + (before === null ? 'entered' : '「' + before + '」→') + ' scene「' + scene + '」'
274
- : 'OK: left the scene「' + (before || '') + '」';
273
+ ? 'OK: ' + (before === null ? 'entered' : 'left "' + before + '", entered ') + 'scene "' + scene + '"'
274
+ : 'OK: left the scene "' + (before || '') + '"';
275
275
  const parts = [];
276
276
  if (count(sw.mcpServers) || count(sw.skillSources)) {
277
277
  parts.push('switched ' + count(sw.mcpServers) + ' MCP server(s) / ' + count(sw.skillSources) + ' skill source(s)');
@@ -280,16 +280,16 @@ export function buildSceneTools(deps) {
280
280
  // 原因,用户问"刚才改了什么"时拿回去的就是半截话。引擎只在**名单真的换了**时才回方案名。
281
281
  const tt = toolTableLabel(mode && mode.toolTablePreset);
282
282
  if (tt) {
283
- parts.push('工具表切到方案「' + tt + '」(关 ' + count(sw.toolTableHidden) + ' 条)');
283
+ parts.push('tool table switched to preset "' + tt + '" (' + count(sw.toolTableHidden) + ' hidden)');
284
284
  }
285
285
  else if (action === 'exit' && mode.restored && mode.restored.toolTable === true) {
286
- parts.push('工具表按进场景前的快照还原');
286
+ parts.push('tool table restored from the pre-scene snapshot');
287
287
  }
288
288
  if (Array.isArray(mode.stale) && mode.stale.length) {
289
- parts.push('丢弃了本机不存在的键:' + mode.stale.join('、'));
289
+ parts.push('dropped keys this machine does not have: ' + mode.stale.join(', '));
290
290
  }
291
291
  return head + (parts.length ? ' · ' + parts.join(' · ') : '') + agentsMdNote(act.agentsMd) +
292
- (noop ? '\n(' + (action === 'enter' ? '目标就是当前场景' : '本来就不在任何场景') + ',运行时没有改动)' : '');
292
+ (noop ? '\n(' + (action === 'enter' ? 'already in the target scene' : 'already outside every scene') + ' — runtime unchanged)' : '');
293
293
  },
294
294
  }));
295
295
  register(defineTool({
@@ -301,7 +301,7 @@ export function buildSceneTools(deps) {
301
301
  parameters: {
302
302
  scene: { type: 'string', required: true, description: 'Scene name (one path segment, ≤64 chars, no / \\ or < > : " | ? *, not `_shared`). Created if absent.' },
303
303
  label: { type: 'string', description: 'Display name for this scene. Omit = keep.' },
304
- description: { type: 'string', description: 'What this scene is for; shown beside the scene name in the「当前场景」reminder while the scene is on — write an instruction, not a note. Omit = keep.' },
304
+ description: { type: 'string', description: 'What this scene is for; shown beside the scene name in the "Current scene" reminder while the scene is on — write an instruction, not a note. Omit = keep.' },
305
305
  // 与下面 `mcp` / `skills` / `subagents` 同一句式("Omit = keep; X = 一个都不"):
306
306
  // 界面 2026-10-10 补了「无」这个选项,工具侧的空串语义要跟着说清,否则模型只知道
307
307
  // 「省略 = 保持」,不知道还有一条「解绑」的路。
@@ -344,12 +344,12 @@ export function buildSceneTools(deps) {
344
344
  throw new Error(blocked);
345
345
  const scene = String((args && args.scene) || '').trim();
346
346
  if (scene === '')
347
- throw new Error('缺少参数:scene 是场景名');
347
+ throw new Error('missing argument: scene (the scene name)');
348
348
  // 场景名是**单个路径段**(界面把场景当一级列表展示,场景名就是 `memories/<场景>/` 那级
349
349
  // 目录名)。`rules-create-scene` 会做完整校验,但档案那条路不经过它 —— 这里先挡一次,
350
350
  // 报错也更贴模型要的东西(它要的是"这个名字不行",不是内部规则)。
351
351
  if (scene.indexOf('/') >= 0 || scene.indexOf('\\') >= 0) {
352
- throw new Error('场景名是单个路径段,不能含 / 或 \\:' + scene);
352
+ throw new Error('a scene name is a single path segment (no / or \\): ' + scene);
353
353
  }
354
354
  // ① 场景记录:`rules-create-scene` 本身是**幂等 upsert**(已存在则更新描述/标签/绑定的
355
355
  // 预设,不报错),所以不必先查再分建/改 —— 与记忆 save 的"有则改、无则建"同一口径。
@@ -371,7 +371,7 @@ export function buildSceneTools(deps) {
371
371
  }
372
372
  const created = await deps.rulesOps['rules-create-scene'](payload);
373
373
  if (!created || created.ok === false)
374
- throw new Error((created && created.error) || '保存场景失败');
374
+ throw new Error((created && created.error) || 'failed to save the scene');
375
375
  // ② 两处「解绑」只能走 `rules-update-scene`:`scene-archive-save` 对载荷里**不带的栏**
376
376
  // 一律保留现值(archive-engine 的「不带 ≠ 解绑」),从现值复制出来的合并档案会把
377
377
  // 刚解掉的绑定又写回去 —— 所以解绑与设置分成两条路,合并前还要先摘掉刚解掉的键。
@@ -383,7 +383,7 @@ export function buildSceneTools(deps) {
383
383
  const setToolTable = args.toolTablePreset !== undefined && String(args.toolTablePreset).trim() !== '';
384
384
  const setQuickPrompts = args.quickPrompts !== undefined;
385
385
  if (setQuickPrompts && !Array.isArray(args.quickPrompts)) {
386
- throw new Error('quickPrompts 必须是 id 数组([] = 进入后全部停用;解绑需用户手动操作,本工具不提供)');
386
+ throw new Error('quickPrompts must be an array of ids ([] = turn them all off on entry; unbinding is a manual step in the panel, not offered here)');
387
387
  }
388
388
  const touched = ARCHIVE_SECTIONS.filter((k) => args[k] !== undefined);
389
389
  const mustWriteArchive = touched.length > 0 || setToolTable || setQuickPrompts;
@@ -396,7 +396,7 @@ export function buildSceneTools(deps) {
396
396
  const cur = await deps.archiveOps['scene-mode-get']({});
397
397
  if (!cur || cur.ok === false) {
398
398
  if (mustWriteArchive)
399
- throw new Error((cur && cur.error) || '读取场景档案失败');
399
+ throw new Error((cur && cur.error) || 'failed to read the scene archive');
400
400
  }
401
401
  else {
402
402
  before = (cur.archives || {})[scene] || {};
@@ -413,7 +413,7 @@ export function buildSceneTools(deps) {
413
413
  const u = await deps.rulesOps['rules-update-scene'](upd);
414
414
  if (!u || u.ok === false) {
415
415
  // 主路径已成功、解绑没写上:如实报,别让回执假装绑定已经没了。
416
- clearWarn = '\nWARN: 解绑未写入(' + ((u && u.error) || '未知错误') + '),旧绑定仍然有效';
416
+ clearWarn = '\nWARN: the unbind was not written (' + ((u && u.error) || 'unknown error') + '); the old binding still applies';
417
417
  }
418
418
  }
419
419
  // ③ 档案 read-modify-write:段(mcp / mcpNotes / skills / subagents)与两个绑定栏的
@@ -440,32 +440,32 @@ export function buildSceneTools(deps) {
440
440
  archive.quickPrompts = quickPromptIds(args.quickPrompts);
441
441
  const r = await deps.archiveOps['scene-archive-save']({ scene, archive });
442
442
  if (!r || r.ok === false)
443
- throw new Error((r && r.error) || '保存场景档案失败');
443
+ throw new Error((r && r.error) || 'failed to save the scene archive');
444
444
  // 回执必须回显**这一次替换挤掉了什么**。整段替换本身是对的(省略的段不填回就会被
445
445
  // 静默清掉,所以"模型给什么就是什么"),但"给出去的段盖掉了原来的勾选"是**静默**的
446
446
  // —— 不写出来,模型只会看到一句"档案段:mcp",以为只是加了一项。
447
447
  const diffs = touched.map((k) => segmentDiff(k, before[k], args[k]));
448
448
  if (diffs.length) {
449
- archiveNote = '档案段:' + diffs.map((d) => d.text).join(';');
449
+ archiveNote = 'archive segments: ' + diffs.map((d) => d.text).join('; ');
450
450
  if (diffs.some((d) => d.dropped.length > 0)) {
451
- warnTail += '\n注意:这些段是整段替换,上面标 - 的项已被挤掉;要保留就把它们一并写进这次调用。';
451
+ warnTail += '\nnote: these segments are replaced as a whole; entries marked "-" above were dropped. To keep them, include them in this same call.';
452
452
  }
453
453
  }
454
454
  // 当前环境里不存在的键会被 op 丢弃并报告(改名或删除留下的,含工具表方案名与
455
455
  // 快捷提示词 id):如实说,否则模型以为勾上了而实际没有。
456
456
  if (r.stale && r.stale.length)
457
- warnTail += '\n(丢弃了当前环境不存在的键:' + r.stale.join('、') + ')';
457
+ warnTail += '\n(dropped keys the current environment does not have: ' + r.stale.join(', ') + ')';
458
458
  // 存盘成功但应用到运行时失败:`scene-archive-save` 对"正是当前模式的那个场景"会就地
459
459
  // 重应用,失败不回滚档案。如实报出去,别假装生效了(与界面同口径)。
460
460
  if (r.applyError)
461
- warnTail += '\n(已存盘,但应用到运行时失败:' + r.applyError + ')';
461
+ warnTail += '\n(saved, but applying it to the runtime failed: ' + r.applyError + ')';
462
462
  }
463
463
  // 解绑写失败了就别输出「X → 解绑」—— 上面那句 WARN 才是事实;设置方向不受 ② 影响。
464
464
  if (args.toolTablePreset !== undefined && !(unbindToolTable && clearWarn !== '')) {
465
465
  toolTableNote = beforeKnown
466
- ? '工具表方案:' + (toolTableLabel(before.toolTablePreset) || '未绑') + ' → ' +
467
- (unbindToolTable ? '解绑' : toolTableLabel(String(args.toolTablePreset).trim()))
468
- : '工具表方案:已解绑(档案现状读不到,无法对比)';
466
+ ? 'tool-table preset: ' + (toolTableLabel(before.toolTablePreset) || 'unbound') + ' -> ' +
467
+ (unbindToolTable ? 'unbind' : toolTableLabel(String(args.toolTablePreset).trim()))
468
+ : 'tool-table preset: unbound (the current archive is unreadable, no diff available)';
469
469
  }
470
470
  if (setQuickPrompts && beforeKnown) {
471
471
  const beforeIds = Array.isArray(before.quickPrompts) ? before.quickPrompts.map((x) => String(x)) : [];
@@ -477,15 +477,15 @@ export function buildSceneTools(deps) {
477
477
  marks.push('+' + toolNameList(added));
478
478
  if (dropped.length)
479
479
  marks.push('-' + toolNameList(dropped));
480
- quickNote = '快捷提示词:' + beforeIds.length + ' 条 → ' + afterIds.length + ' 条' +
481
- (afterIds.length === 0 ? '(进入后全部停用)' : '') +
482
- (marks.length ? '(' + marks.join(' ') + ')' : '');
480
+ quickNote = 'quick prompts: ' + beforeIds.length + ' -> ' + afterIds.length + ' ids' +
481
+ (afterIds.length === 0 ? ' (all off after entering)' : '') +
482
+ (marks.length ? ' (' + marks.join(' ') + ')' : '');
483
483
  }
484
484
  // 收敛提示:`rules-create-scene` 在"历史默认(全部启用)"状态下会把启用集合收窄成
485
485
  // 单选(`collapseActiveForNewScene`)—— 那是一次**会改变运行时**的动作,必须说出来。
486
486
  // 界面上同样如实报(`scenes.result.created.collapsed`)。
487
487
  const collapsed = created.collapsedActive === true;
488
- const parts = [touchedRecord.length ? '场景记录已更新(' + touchedRecord.join('、') + ')' : '场景已确保存在'];
488
+ const parts = [touchedRecord.length ? 'scene record updated (' + touchedRecord.join(', ') + ')' : 'scene existence ensured'];
489
489
  if (archiveNote)
490
490
  parts.push(archiveNote);
491
491
  if (toolTableNote)
@@ -496,13 +496,13 @@ export function buildSceneTools(deps) {
496
496
  // 部分启用(开 save 不开 switch)时指向一条模型没有的工具,只会让它白跑一趟 ——
497
497
  // 与 mcp/skill 的 listHint 同一条纪律。
498
498
  const enterHint = deps.toolVisible('scene_manager_switch')
499
- ? '要让它生效,用 scene_manager_switch 进入。'
500
- : '要让它生效,需用户手动进入该场景(本会话未开放 scene_manager_switch)。';
501
- return 'OK: scene「' + scene + '」' + parts.join(';') + warnTail + clearWarn +
499
+ ? 'To take effect, enter it with scene_manager_switch.'
500
+ : 'To take effect, the user has to enter this scene manually (scene_manager_switch is not available in this session).';
501
+ return 'OK: scene "' + scene + '"' + parts.join('; ') + warnTail + clearWarn +
502
502
  (collapsed
503
- ? '\n注意:此前所有场景都处于启用状态(历史「全部启用」默认),本次已收窄为只启用一个,新建的这个不在其中 —— 需用户确认启用状态。'
503
+ ? '\nnote: every scene was enabled until now (the historical all-enabled default); this call narrowed the enabled set to one scene, and the new one is not it — the user should confirm the enabled state.'
504
504
  : '') +
505
- '\n保存不改运行时:' + enterHint;
505
+ '\nSaving does not change the runtime: ' + enterHint;
506
506
  },
507
507
  }));
508
508
  }