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
package/lib/tools/prompt.js
CHANGED
|
@@ -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
|
|
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('
|
|
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
|
|
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 比对版本,写文件即下一轮重读 —— 不是"下个会话"。说错了
|
package/lib/tools/remove.js
CHANGED
|
@@ -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
|
|
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
|
-
? '
|
|
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('
|
|
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 + '
|
|
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:
|
|
79
|
-
(name === 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
|
};
|
package/lib/tools/scene.js
CHANGED
|
@@ -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 ? '
|
|
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 '
|
|
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
|
-
'
|
|
77
|
-
'
|
|
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('
|
|
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('
|
|
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 ? '
|
|
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 + '
|
|
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:
|
|
136
|
+
return '\nWARN: the global baseline (AGENTS.md) was not synced: ' + String(sync.error);
|
|
137
137
|
if (sync.applied)
|
|
138
|
-
return '\nAGENTS.md
|
|
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
|
|
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
|
|
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 ? '
|
|
193
|
-
const bits = ['- ' + title + ' [' + state.join('
|
|
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('
|
|
195
|
+
bits.push('description: ' + String(s.description));
|
|
196
196
|
if (s.prompt)
|
|
197
|
-
bits.push('
|
|
198
|
-
bits.push('
|
|
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 '
|
|
202
|
-
(r.activeMode === 'all' ? '
|
|
203
|
-
(entered === null ? '
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
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' ? '
|
|
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' ? '
|
|
256
|
-
'
|
|
257
|
-
'
|
|
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
|
|
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' : '
|
|
274
|
-
: 'OK: left the scene
|
|
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('
|
|
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('
|
|
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
|
|
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
|
|
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('
|
|
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('
|
|
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
|
|
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:
|
|
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 = '
|
|
449
|
+
archiveNote = 'archive segments: ' + diffs.map((d) => d.text).join('; ');
|
|
450
450
|
if (diffs.some((d) => d.dropped.length > 0)) {
|
|
451
|
-
warnTail += '\
|
|
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
|
|
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
|
|
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
|
-
? '
|
|
467
|
-
(unbindToolTable ? '
|
|
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 = '
|
|
481
|
-
(afterIds.length === 0 ? '
|
|
482
|
-
(marks.length ? '
|
|
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 ? '
|
|
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
|
-
? '
|
|
500
|
-
: '
|
|
501
|
-
return 'OK: scene
|
|
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
|
-
? '\
|
|
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
|
-
'\
|
|
505
|
+
'\nSaving does not change the runtime: ' + enterHint;
|
|
506
506
|
},
|
|
507
507
|
}));
|
|
508
508
|
}
|