dsh-session-conductor 1.3.13 → 1.3.17

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 (87) hide show
  1. package/README.md +55 -1
  2. package/cordis.patch.yml +0 -0
  3. package/lib/apply.js +28 -1243
  4. package/lib/client-parts/apply.js +0 -0
  5. package/lib/client-parts/components/compaction.js +0 -0
  6. package/lib/client-parts/components/conductor-settings.js +0 -0
  7. package/lib/client-parts/components/group.js +0 -0
  8. package/lib/client-parts/components/panel.js +0 -0
  9. package/lib/client-parts/components/processing.js +0 -0
  10. package/lib/client-parts/components/session-icon.js +0 -0
  11. package/lib/client-parts/components/settings.js +0 -0
  12. package/lib/client-parts/foundation/bootstrap.js +0 -0
  13. package/lib/client-parts/foundation/i18n.js +0 -0
  14. package/lib/client-parts/foundation/list-cache.js +0 -0
  15. package/lib/client-parts/foundation/styles.js +0 -0
  16. package/lib/core/config.js +0 -0
  17. package/lib/core/domain.js +0 -0
  18. package/lib/core/index-store.js +0 -0
  19. package/lib/core/lazy.js +0 -0
  20. package/lib/core/state.js +0 -0
  21. package/lib/core.js +0 -0
  22. package/lib/eio-repair.js +0 -0
  23. package/lib/features/base-routes.js +156 -0
  24. package/lib/features/context-usage/index.js +0 -0
  25. package/lib/features/continue/eligibility.js +0 -0
  26. package/lib/features/continue/scan.js +0 -0
  27. package/lib/features/continue/session.js +0 -0
  28. package/lib/features/inject/preset.js +0 -0
  29. package/lib/features/member-model/batch.js +97 -0
  30. package/lib/features/member-model/clone-member.js +270 -0
  31. package/lib/features/member-model/command.js +66 -7
  32. package/lib/features/member-model/create-teammate.js +2 -2
  33. package/lib/features/member-model/index.js +17 -1
  34. package/lib/features/member-model/lead.js +73 -0
  35. package/lib/features/member-model/locate.js +82 -1
  36. package/lib/features/member-model/override.js +0 -0
  37. package/lib/features/member-model/permission-command.js +138 -0
  38. package/lib/features/member-model/permission.js +213 -0
  39. package/lib/features/member-model/setup.js +0 -0
  40. package/lib/features/member-model/switch.js +137 -0
  41. package/lib/features/member-model/tools.js +359 -0
  42. package/lib/features/memory-recall/index.js +0 -0
  43. package/lib/features/rename/analysis.js +0 -0
  44. package/lib/features/rename/title.js +0 -0
  45. package/lib/features/rename/value.js +0 -0
  46. package/lib/features/session-action-routes.js +435 -0
  47. package/lib/features/session-messaging/index.js +0 -0
  48. package/lib/features/session-messaging/logic.js +0 -0
  49. package/lib/features/session-messaging/tools.js +0 -0
  50. package/lib/features/session-ops-routes.js +0 -0
  51. package/lib/features/settings-routes.js +411 -0
  52. package/lib/features/trash.js +0 -0
  53. package/lib/features/value-index.js +0 -0
  54. package/lib/features/value-report-routes.js +211 -0
  55. package/lib/group.js +0 -0
  56. package/lib/i18n/en.json +0 -0
  57. package/lib/i18n/zh.json +0 -0
  58. package/lib/index.js +0 -0
  59. package/lib/repair.js +0 -0
  60. package/lib/seq-gap-repair.js +0 -0
  61. package/lib/session-codec.js +0 -0
  62. package/lib/session-log.js +0 -0
  63. package/lib/sessions/detach.js +0 -0
  64. package/lib/sessions/events.js +0 -0
  65. package/lib/sessions/interruption.js +0 -0
  66. package/lib/sessions/list-cache.js +0 -0
  67. package/lib/sessions/list.js +0 -0
  68. package/lib/sessions/lookup.js +0 -0
  69. package/lib/sessions/route.js +0 -0
  70. package/lib/sessions/session-id.js +0 -0
  71. package/lib/sessions/tail-io.js +30 -0
  72. package/lib/sessions/turn.js +0 -0
  73. package/lib/shared/atomic-file.js +0 -0
  74. package/lib/shared/constants.js +0 -0
  75. package/lib/shared/json-store.js +0 -0
  76. package/lib/shared/log.js +0 -0
  77. package/lib/shared/timing.js +0 -0
  78. package/lib/shared/util.js +0 -0
  79. package/lib/template-inject.js +0 -0
  80. package/lib/value.js +0 -0
  81. package/lib/zstd-frames.js +0 -0
  82. package/package.json +1 -1
  83. package/scripts/check-member-model.mjs +0 -0
  84. package/skills/dsh-session-conductor-functions.md +0 -0
  85. package/skills/dsh-session-conductor.md +0 -0
  86. package/skills/dsh-session-official-doc.md +0 -0
  87. package/skills/task-completion-report.md +0 -0
@@ -0,0 +1,359 @@
1
+ // dsh-session-conductor — 成员模型/权限相关的 AI 工具与聊天指令注册
2
+ //
3
+ // 【为什么单独成文件】原先这 293 行全部内联在 lib/apply.js 的 `ctx.inject(["tools"])` 回调里,
4
+ // 使该回调变成一个巨型函数(审计 readability/max-function-length 拦截),
5
+ // 而它内部其实只有两类内容:① 5 个工具的注册(list_models / set_member_model /
6
+ // list_permission_presets / set_member_permission / create_teammate_with_model)
7
+ // ② 两条聊天指令的注册(/model-switch、/perm-switch)。两者都属于「成员模型/权限」这一域,
8
+ // 整体搬出来即可,handler 体与错误隔离策略逐字保留。
9
+ //
10
+ // 【错误隔离约定(必须保留)】每个工具单独包 try/catch:defineTool 抛错会让整个
11
+ // inject 回调中止,历史上因此导致四个工具一起消失(宿主侧表现为「tool 不存在」)。
12
+ import { log } from "../../shared/log.js";
13
+ import { findTargetAgent, resolveTargets, validateModelPair } from "./locate.js";
14
+ import { switchAgentModel, registerSwitchWatch, readLastRequestModel } from "./switch.js";
15
+ import { createTeammateWithModel, renderCreateTeammateResult } from "./create-teammate.js";
16
+ import { switchMemberPermission, describeMemberPermission, readPresetCatalog } from "./permission.js";
17
+ import { readLeaderRoute } from "./lead.js";
18
+ import { applyModelToMany, applyPermissionToMany, renderBatchReport } from "./batch.js";
19
+ import { registerModelSwitchCommand } from "./command.js";
20
+ import { registerContextUsageTool } from "../context-usage/index.js";
21
+ import { registerMemoryRecallTool } from "../memory-recall/index.js";
22
+ import { registerSessionMessagingTools } from "../session-messaging/index.js";
23
+ import { registerPermSwitchCommand } from "./permission-command.js";
24
+ import { cloneMemberAs, renderCloneMemberResult } from "./clone-member.js";
25
+
26
+ /**
27
+ * 注册成员模型/权限相关的全部 AI 工具与聊天指令。
28
+ *
29
+ * @param {object} deps
30
+ * @param {object} deps.ctx 插件 context
31
+ * @param {object} deps.tools 宿主 tools 服务(提供 register)
32
+ * @param {Function} deps.defineTool 宿主 defineTool(工作区自测环境可能为 undefined,调用方已判空)
33
+ * @returns {void}
34
+ */
35
+ export function registerMemberModelTools({ ctx, tools, defineTool }) {
36
+ tools.register(defineTool({
37
+ name: "list_models",
38
+ description: "列出当前可用模型(provider → models[])。切模型前先用它拿到确切 model id,不要凭记忆猜模型名。",
39
+ parameters: {},
40
+ output: {
41
+ schema: { type: "string" },
42
+ render(args, value) {
43
+ return [{ type: "text", text: String(value) }];
44
+ }
45
+ },
46
+ async execute() {
47
+ try {
48
+ const sc = ctx.get("sessionController");
49
+ if (!sc?.modelCatalog) return "modelCatalog 服务不可用(无法列出模型)";
50
+ const catalog = await sc.modelCatalog();
51
+ const groups = catalog?.groups ?? [];
52
+ if (groups.length === 0) return "模型目录为空(检查 provider 配置)";
53
+ return groups.map((g) => {
54
+ const models = (g.models ?? [])
55
+ .map((m) => ` - ${m.id}${m.name && m.name !== m.id ? `(${m.name})` : ""}`)
56
+ .join("\n");
57
+ return `${g.id}${g.name && g.name !== g.id ? `(${g.name})` : ""}:\n${models}`;
58
+ }).join("\n\n");
59
+ } catch (error) {
60
+ return `列出模型失败:${String(error?.message ?? error)}`;
61
+ }
62
+ }
63
+ }));
64
+ /**
65
+ * 切换后的「查证快照」:最近一次实际请求 + 最近一次切换意图 + 一行判定。
66
+ * 抽出来是因为这段有 4 个分支,塞在工具 execute 里把圈复杂度顶到 18;
67
+ * 判定只认「请求时真实使用的模型」(request/header),不信注入的提示语。
68
+ * @returns {Promise<{snap: string, verdict: string}>}
69
+ */
70
+ async function describeSwitchSnapshot(ctx, sessionId, provider, model) {
71
+ const startedAt = Date.now();
72
+ const { lastRequest, lastSelection, scanned } = await readLastRequestModel(sessionId);
73
+ // 读整份日志取「最近一次请求」,大日志上这一步本身就要时间
74
+ log(ctx, `切换查证快照耗时 ${Date.now() - startedAt}ms(命中请求记录 ${scanned ?? 0} 条)`);
75
+ if (!lastRequest) {
76
+ return {
77
+ snap: "【查证·最近一次实际请求】会话日志里还没有 request/header 记录(该成员可能从未运行过)",
78
+ verdict: "【查证·判定】⏳ 待该成员首次运行后生效",
79
+ };
80
+ }
81
+ const when = lastRequest.time ? `(${new Date(lastRequest.time).toLocaleString("zh-CN")})` : "";
82
+ const snap = `【查证·最近一次实际请求】${lastRequest.provider}/${lastRequest.model}${when}`
83
+ + (lastSelection ? `\n【查证·最近一次切换意图】${lastSelection.provider}/${lastSelection.model}` : "");
84
+ const alreadyNew = lastRequest.provider === provider && lastRequest.model === model;
85
+ return {
86
+ snap,
87
+ verdict: alreadyNew
88
+ ? "【查证·判定】✅ 该成员最近的请求已经是新模型"
89
+ : "【查证·判定】⏳ 待下一回合生效:最近一次请求是切换前发出的(属正常,不代表失败)",
90
+ };
91
+ }
92
+
93
+ tools.register(defineTool({
94
+ name: "set_member_model",
95
+ description: "给成员/子代理切换模型。三个参数都可空:target 留空 = 作用于全部成员(慎用,会改所有子代理);provider/model 留空 = 跟随主代理(Team Lead)当前实际使用的模型。target 传成员名/sessionId/标题关键字。切换前会校验 provider/model 组合是否存在。批量时逐条执行、回执带完整名单。",
96
+ parameters: {
97
+ target: { type: "string", description: "目标成员:成员名、sessionId 或标题关键字。留空 = 全部成员(排除 Lead)" },
98
+ provider: { type: "string", description: "provider 分组名(list_models 的顶层分组)。留空 = 跟随主代理的 provider" },
99
+ model: { type: "string", description: "该 provider 下的模型 id(含前缀)。留空 = 跟随主代理的 model" }
100
+ },
101
+ output: {
102
+ schema: { type: "string" },
103
+ render(args, value) {
104
+ return [{ type: "text", text: String(value) }];
105
+ }
106
+ },
107
+ async execute(args, exec) {
108
+ const caller = exec?.agent ?? null;
109
+ let provider = String(args?.provider ?? "").trim();
110
+ let model = String(args?.model ?? "").trim();
111
+ // provider/model 留空 = 跟随主代理(Team Lead)当前实际使用的模型。
112
+ // 取值只认「请求级权威记录」,读不到就如实报错,绝不用名册里的静态字段冒充。
113
+ let followedLead = null;
114
+ if (!provider || !model) {
115
+ const lead = await readLeaderRoute(ctx, { caller });
116
+ if (lead.error) return `切换失败:${lead.error}`;
117
+ provider = lead.provider;
118
+ model = lead.model;
119
+ followedLead = lead;
120
+ }
121
+ // 先校验组合有效性——无效组合会让后续回合的模型请求被拒(表现为「切换生效但回合失败」)
122
+ const pair = await validateModelPair(ctx, provider, model);
123
+ if (pair.error) return `切换失败:${pair.error}`;
124
+ // target 留空 = 作用于全部成员;与聊天指令 /model-switch 走同一套目标解析
125
+ const resolved = resolveTargets(ctx, args?.target, { caller });
126
+ if (resolved.error) return `切换失败:${resolved.error}`;
127
+ if (resolved.all) {
128
+ const report = await applyModelToMany(ctx, resolved.targets, { provider, model });
129
+ const extra = [];
130
+ if (followedLead) {
131
+ extra.push(`模型来源:跟随主代理 ${followedLead.sessionId}(${followedLead.modelSource})`);
132
+ for (const w of followedLead.warnings ?? []) extra.push(`提示:${w}`);
133
+ }
134
+ if (pair.note) extra.push(`【校验】${pair.note}`);
135
+ return renderBatchReport(`批量切换模型 → ${provider}/${model}`, report, extra);
136
+ }
137
+ const found = resolved.targets[0];
138
+ const switchResult = await switchAgentModel(ctx, found.sessionId, provider, model, found.agent);
139
+ if (switchResult.error) return `切换失败:${switchResult.error}`;
140
+ const memberLabel = found.member?.name ? `${found.member.name}(${found.sessionId})` : found.sessionId;
141
+ // 查证(注册 job,不阻塞):登记待查证项 → 该成员下次发起请求时由 agent/request 事件判定
142
+ // → 结果注入回本次调用方。同时立刻附一份快照:最近一次实际请求 + 最近一次切换意图。
143
+ // 判定只认「请求时真实使用的模型」(request/header),不信注入的提示语。
144
+ try {
145
+ registerSwitchWatch(found.sessionId, provider, model, exec?.agent?.session?.id ?? null);
146
+ } catch (error) {
147
+ log(ctx, `切换查证登记失败(不影响本次切换): ${String(error?.message ?? error)}`);
148
+ }
149
+ let snap = "";
150
+ let verdict = "";
151
+ try {
152
+ ({ snap, verdict } = await describeSwitchSnapshot(ctx, found.sessionId, provider, model));
153
+ } catch (error) {
154
+ snap = `【查证·快照】读取会话日志失败:${String(error?.message ?? error)}`;
155
+ }
156
+ // 复核命令按**当前插件安装位置**生成(不写死本机路径),可直接复制执行
157
+ const scriptUrl = new URL("../scripts/check-member-model.mjs", import.meta.url);
158
+ const cmd = `node ${decodeURIComponent(scriptUrl.pathname)} ${found.sessionId}`;
159
+ return [
160
+ `已把成员 ${memberLabel}(${found.matched} 匹配)切到 ${provider}/${model};生效方式:${switchResult.via}`,
161
+ // 组合校验默认关闭(避免 @Remote 卡顿);跳过时如实告知,别让人以为「拼错也会被拦住」
162
+ pair.note ? `【校验】${pair.note}` : "",
163
+ snap,
164
+ verdict,
165
+ "【查证·任务】已登记(不阻塞):该成员下次发起请求时按事件判定,结果会在下一次 step 注入给你",
166
+ `复核命令(可直接复制):\n${cmd}`,
167
+ ].filter(Boolean).join("\n");
168
+ }
169
+ }));
170
+ // 列出宿主当前可选的权限预设。存在的意义是让 set_member_permission 的 preset 参数
171
+ // 有可查来源,避免凭记忆猜名字(不可用的预设名写进会话会让成员加载失败)。
172
+ try {
173
+ tools.register(defineTool({
174
+ name: "list_permission_presets",
175
+ description: "列出宿主当前可选的权限预设(permission preset)名称与默认值。给成员切换权限前先用它确认确切名字。",
176
+ parameters: {},
177
+ output: {
178
+ schema: { type: "string" },
179
+ render(args, value) {
180
+ return [{ type: "text", text: String(value) }];
181
+ }
182
+ },
183
+ async execute() {
184
+ const catalog = await readPresetCatalog(ctx);
185
+ if (catalog.error) return `读取失败:${catalog.error}`;
186
+ const lines = [`可选权限预设(共 ${catalog.names.length} 个)`, ""];
187
+ for (const name of catalog.names) lines.push(` - ${name}`);
188
+ if (catalog.defaultPreset) lines.push("", `新会话默认预设:${catalog.defaultPreset}`);
189
+ if (catalog.names.includes("auto")) {
190
+ lines.push("", "注意:auto 依赖「自动审查」集成;该功能一旦关闭,带 auto 的会话会无法恢复(宿主报 subagent/not-resumable)。");
191
+ }
192
+ return lines.join("\n");
193
+ }
194
+ }));
195
+ } catch (error) {
196
+ log(ctx, `list_permission_presets 工具注册失败(不影响其它工具): ${String(error?.message ?? error)}`);
197
+ }
198
+ // 查看/切换成员(子代理)的权限预设。
199
+ // 子代理会话在界面上无法手动切权限预设(界面只对主会话开放),而 preset 一旦指向
200
+ // 当前不可用的值(典型:auto 在「自动审查」关闭后悬空),该成员就永久唤不醒——
201
+ // 宿主 permission-presets 在会话发布前抛错,再被 subagent 的 coldResume 统一包成
202
+ // `subagent/not-resumable`,报错文本完全指不到权限层。此工具提供不改界面的修正通道。
203
+ // 与其它工具一致:render 必须放在 output 里;单独包 try/catch,避免一个工具抛错
204
+ // 让整个 inject("tools") 中止(历史上四个工具一起消失过)。
205
+ try {
206
+ tools.register(defineTool({
207
+ name: "set_member_permission",
208
+ description: "给成员/子代理切换权限预设(写在该成员会话日志里,下次加载生效)。子代理无法在界面上手动切换权限预设,只能用这个工具。两个参数都可空:target 留空 = 作用于全部成员(慎用);preset 留空 = 跟随主代理(Team Lead)当前预设。preset 必须在宿主当前可选列表里,可用 list_permission_presets 查。批量时逐条执行、回执带完整名单。",
209
+ parameters: {
210
+ target: { type: "string", description: "目标成员:成员名、sessionId 或标题关键字。留空 = 全部成员(排除 Lead)" },
211
+ preset: { type: "string", description: "目标权限预设名(如 danger-full-access)。留空 = 跟随主代理的预设" }
212
+ },
213
+ output: {
214
+ schema: { type: "string" },
215
+ render(args, value) {
216
+ return [{ type: "text", text: String(value) }];
217
+ }
218
+ },
219
+ async execute(args, exec) {
220
+ const caller = exec?.agent ?? null;
221
+ let preset = String(args?.preset ?? "").trim();
222
+ // preset 留空 = 跟随主代理(Team Lead)当前预设;主代理自己没记录预设时如实报错,
223
+ // 不猜一个默认值塞进去(写错预设会让成员加载失败)。
224
+ let followedLead = null;
225
+ if (!preset) {
226
+ const lead = await readLeaderRoute(ctx, { caller });
227
+ if (lead.error) return `切换失败:${lead.error}`;
228
+ if (!lead.preset) return `切换失败:主代理(${lead.sessionId})没有记录权限预设,无法跟随;请显式指定 preset(可用 list_permission_presets 查)`;
229
+ preset = lead.preset;
230
+ followedLead = lead;
231
+ }
232
+ // target 留空 = 作用于全部成员
233
+ const resolved = resolveTargets(ctx, args?.target, { caller });
234
+ if (resolved.error) return `切换失败:${resolved.error}`;
235
+ if (resolved.all) {
236
+ const report = await applyPermissionToMany(ctx, resolved.targets, preset);
237
+ const extra = [];
238
+ if (followedLead) {
239
+ extra.push(`预设来源:跟随主代理 ${followedLead.sessionId}(${followedLead.preset})`);
240
+ for (const w of followedLead.warnings ?? []) extra.push(`提示:${w}`);
241
+ }
242
+ return renderBatchReport(`批量切换权限预设 → ${preset}`, report, extra);
243
+ }
244
+ const found = resolved.targets[0];
245
+ const label = found.member?.name ? `${found.member.name}(${found.sessionId})` : found.sessionId;
246
+ const result = await switchMemberPermission(ctx, found.sessionId, preset);
247
+ if (result.error) return `切换失败:${result.error}`;
248
+ const lines = [
249
+ `已把成员 ${label} 的权限预设切成 ${result.to}`,
250
+ `原预设:${result.from ?? "(未记录)"} | 新事件 seq:${result.seq}`,
251
+ ];
252
+ if (followedLead) lines.push(`预设来源:跟随主代理 ${followedLead.sessionId}`);
253
+ if (result.note) lines.push(`说明:${result.note}`);
254
+ lines.push(`当前旋钮:sandbox=${result.knobs?.sandbox ?? "(未记录)"} | approval=${result.knobs?.approval ?? "(未记录)"}`);
255
+ lines.push("改动写在会话日志里,成员下次加载/唤醒时按折叠语义生效(最后一条 preset 事件为准)。");
256
+ return lines.join("\n");
257
+ }
258
+ }));
259
+ } catch (error) {
260
+ log(ctx, `set_member_permission 工具注册失败(不影响其它工具): ${String(error?.message ?? error)}`);
261
+ }
262
+ // 创建成员并同时指定模型:宿主 spawn_teammate 没有 provider/model 参数,这里补齐。
263
+ // 形状必须与其它工具一致(render 放在 output 里);且单独包 try/catch——
264
+ // 这个回调里任何一个 defineTool 抛错都会让整个 inject("tools") 中止,四个工具一起消失。
265
+ try {
266
+ tools.register(defineTool({
267
+ name: "create_teammate_with_model",
268
+ description: "创建团队成员,并让第一轮真实任务就用指定模型(宿主 spawn_teammate 没有 provider/model 参数,此工具补齐)。内部流程:占位任务创建 → 立刻中断(避免用默认模型把首轮跑掉)→ 写 model/selection → 投递真实任务。只能由 Team Lead 调用。",
269
+ parameters: {
270
+ name: { type: "string", required: true, description: "成员名(lower-kebab-case,唯一)" },
271
+ description: { type: "string", required: true, description: "职责简述" },
272
+ prompt: { type: "string", required: true, description: "该成员的初始任务全文" },
273
+ provider: { type: "string", required: true, description: "模型 provider 分组名(agnes / wb / d …;先用 list_models 查确切值)" },
274
+ model: { type: "string", required: true, description: "模型 id(先用 list_models 查确切值)" },
275
+ subagentProvider: { type: "string", description: "子代理 provider(与模型 provider 不是一回事;默认取 ctx.subagents.list() 第一个,通常就是 spawn)。一般不用填" },
276
+ context: { type: "string", description: "上下文来源:fresh(默认,不继承)或 fork(继承调用方已有回合)" },
277
+ },
278
+ output: {
279
+ schema: { type: "string" },
280
+ render(args, value) {
281
+ return [{ type: "text", text: String(value) }];
282
+ },
283
+ },
284
+ async execute(args, exec) {
285
+ const result = await createTeammateWithModel(ctx, exec, args);
286
+ return renderCreateTeammateResult(args, result);
287
+ },
288
+ }));
289
+ } catch (error) {
290
+ log(ctx, `create_teammate_with_model 注册失败(不影响其它工具): ${String(error?.message ?? error)}`);
291
+ }
292
+
293
+ // 上下文用量自查工具:让 AI 自己按需查占用与构成,而不是每轮把数值注入系统提示词
294
+ // (每轮注入会让系统提示词前缀每轮变化 → prompt cache 失效 → 成本上升)。
295
+ if (registerContextUsageTool(ctx, defineTool, tools)) {
296
+ log(ctx, "上下文用量工具已注册 (context_usage)");
297
+ }
298
+ // 跨会话消息 + 指定模型新建会话(三个工具一次注册)。
299
+ // 整体包 try/catch:注册期抛错会连累同批其它工具(本项目历史事故),失败只留痕、不阻断。
300
+ try {
301
+ if (registerSessionMessagingTools(ctx, defineTool, tools)) {
302
+ log(ctx, "跨会话工具已注册 (list_sessions / send_to_session / create_session)");
303
+ }
304
+ } catch (error) {
305
+ log(ctx, `跨会话工具注册失败(不影响其它工具): ${String(error?.message ?? error)}`);
306
+ }
307
+ try {
308
+ // 记忆取回的手动通道:让 AI 自己按需回溯(自动注入之外的补充)
309
+ if (registerMemoryRecallTool(ctx, defineTool, tools)) {
310
+ log(ctx, "记忆取回工具已注册 (recall_memory)");
311
+ }
312
+ } catch (error) {
313
+ log(ctx, `记忆取回工具注册失败(不影响其它工具): ${String(error?.message ?? error)}`);
314
+ }
315
+ log(ctx, "成员模型工具已注册 (list_models / set_member_model)");
316
+ // 把成员「换名」成新成员(新建 + 搬会话内容)。
317
+ // 宿主把成员名钉死为不可变身份字段:`team/member` 投影里 name/provider/context 一变就抛
318
+ // `changed immutable identity fields`,且只允许 provisioning → active 这一次写入,
319
+ // 宿主也没有任何 rename 接口。所以换名只能「新建成员 + 搬旧会话内容」。
320
+ // 这条路成立是因为成员名与会话内容分处两地:名字在 Lead 日志的 team/member 事件里,
321
+ // 内容在成员自己的日志里——搬内容不会触碰不可变字段。
322
+ try {
323
+ tools.register(defineTool({
324
+ name: "clone_member_as",
325
+ description: "把一个成员的会话内容克隆到一个新成员(用于换名:宿主不允许改成员名,只能新建+搬内容)。内部流程:解析源成员 → spawn 新成员(占位任务)→ 中断并等它空闲 → 把源会话事件搬进新成员日志(重排 seq、带 checksum、写前自检、自动备份)。只由 Team Lead 调用。旧成员不会被改动,需要脱手请自行删除。",
326
+ parameters: {
327
+ source: { type: "string", required: true, description: "源成员:成员名 / sessionId / 标题关键字" },
328
+ name: { type: "string", required: true, description: "新成员名(lower-kebab-case,不能与现有成员重复)" },
329
+ description: { type: "string", description: "新成员职责简述(默认「克隆自 <源成员>」)" },
330
+ withHistory: { type: "boolean", description: "是否搬历史内容(默认 true)。传 false = 只建空成员" },
331
+ provider: { type: "string", description: "写进新成员 descriptor 的模型 provider(省略则沿用宿主默认)" },
332
+ model: { type: "string", description: "写进新成员 descriptor 的模型 id(省略则沿用宿主默认)" },
333
+ subagentProvider: { type: "string", description: "子代理 provider(默认取 ctx.subagents.list() 第一个,通常就是 spawn)。一般不用填" },
334
+ },
335
+ output: {
336
+ schema: { type: "string" },
337
+ render(args, value) {
338
+ return [{ type: "text", text: String(value) }];
339
+ },
340
+ },
341
+ async execute(args, exec) {
342
+ const result = await cloneMemberAs(ctx, exec, args);
343
+ return renderCloneMemberResult(args, result);
344
+ },
345
+ }));
346
+ log(ctx, "成员克隆工具已注册 (clone_member_as)");
347
+ } catch (error) {
348
+ log(ctx, `成员克隆工具注册失败(不影响其它工具): ${String(error?.message ?? error)}`);
349
+ }
350
+ // 聊天框指令 `/model-switch`:用户自己就能看/切成员模型的入口(与 AI 工具同一套切换 + 查证路径)。
351
+ // 异步注册 + 独立兜底:宿主 commands 服务或 brand 包不可用时只留痕,绝不影响其它功能(同既有约定)。
352
+ registerModelSwitchCommand(ctx)
353
+ .then((ok) => { if (ok) log(ctx, "聊天框指令已注册 (/model-switch)"); })
354
+ .catch((error) => log(ctx, `聊天框指令注册失败(不影响其它功能): ${String(error?.message ?? error)}`));
355
+ // 聊天框指令 `/perm-switch`:子代理在界面上切不了权限预设,只能靠这条写会话日志的通道。
356
+ registerPermSwitchCommand(ctx)
357
+ .then((ok) => { if (ok) log(ctx, "聊天框指令已注册 (/perm-switch)"); })
358
+ .catch((error) => log(ctx, `权限指令注册失败(不影响其它功能): ${String(error?.message ?? error)}`));
359
+ }
File without changes
File without changes
File without changes
File without changes