@morlay/dsh-session-mode 0.0.15 → 0.0.16

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/dist/index.cjs CHANGED
@@ -1,5 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_shared = require("./shared-DO1nZbJ0.cjs");
2
+ const require_rows = require("./rows-BT6Fpq-E.cjs");
3
+ const require_mode_config = require("./mode-config-DOQ1VjcM.cjs");
3
4
  const require_catalog = require("./catalog-C_6kb1aC.cjs");
4
5
  let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
5
6
  let _deepseek_ai_dsh_agent = require("@deepseek-ai/dsh-agent");
@@ -7,12 +8,23 @@ let _deepseek_ai_dsh_llm = require("@deepseek-ai/dsh-llm");
7
8
  let _deepseek_ai_dsh_agent_preset_registry = require("@deepseek-ai/dsh-agent-preset-registry");
8
9
  let zod = require("zod");
9
10
  let _deepseek_ai_schemastery = require("@deepseek-ai/schemastery");
10
- _deepseek_ai_schemastery = require_shared.__toESM(_deepseek_ai_schemastery, 1);
11
+ _deepseek_ai_schemastery = require_mode_config.__toESM(_deepseek_ai_schemastery, 1);
12
+ let _morlay_dsh_tool_guidance_groups = require("@morlay/dsh-tool-guidance/groups");
11
13
  let _deepseek_ai_dsh_system_prompt = require("@deepseek-ai/dsh-system-prompt");
14
+ let _deepseek_ai_dsh_tools = require("@deepseek-ai/dsh-tools");
15
+ let _deepseek_ai_dsh_tool_executor_ptc = require("@deepseek-ai/dsh-tool-executor-ptc");
16
+ let node_fs_promises = require("node:fs/promises");
17
+ let node_path = require("node:path");
18
+ let _deepseek_ai_dsh_home_paths = require("@deepseek-ai/dsh-home-paths");
19
+ let smol_toml = require("smol-toml");
20
+ let _deepseek_ai_dsh_storage_domain = require("@deepseek-ai/dsh-storage-domain");
12
21
  // src/modes.ts
13
22
  function derivedSkills(mode) {
14
23
  return (mode.allowTools.length === 0 || mode.allowTools.includes("skill")) && !mode.denyTools.includes("skill");
15
24
  }
25
+ function normalizeMode(value) {
26
+ return modeSchema(value);
27
+ }
16
28
  const localized = (text) => text;
17
29
  const personaSchema = _deepseek_ai_schemastery.default.object({
18
30
  prefix: _deepseek_ai_schemastery.default.string().default(""),
@@ -35,10 +47,6 @@ const modelSchema = _deepseek_ai_schemastery.default.object({
35
47
  });
36
48
  const modeSchema = _deepseek_ai_schemastery.default.object({
37
49
  preset: _deepseek_ai_schemastery.default.string().hidden(),
38
- presetsOnly: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "agent-presets" }).default([]).description(localized({
39
- zh: "这个模式只允许挂哪些 agent preset(它们的 id,官方或本部署自建的):行清单由挂着的那份提供,几个模式可以共享同一份名单。留空表示**允许全部**——哪份 preset 都行,选这个模式也不动会话当前挂着的那份;有值时当前的已经在名单里就不动,否则换成名单里的第一个。模式自己的提示词、工具收口与开关照常生效。",
40
- en: "Which agent presets this mode may ride on (their ids, shipped or deployment-owned): that preset supplies the row list, and several modes may share one list. Empty means **every preset is allowed** — selecting the mode then leaves the session's current preset alone; when set, the current one stays if it is listed, otherwise the session switches to the first entry. The mode's persona, tool narrowing, and switches apply either way."
41
- })),
42
50
  name: _deepseek_ai_schemastery.default.string().required().description(localized({
43
51
  zh: "模式名(事实文案;选择面归官方 roster)。",
44
52
  en: "Mode name (fact copy; the picker belongs to the official roster)."
@@ -84,6 +92,10 @@ const modeSchema = _deepseek_ai_schemastery.default.object({
84
92
  zh: "是否要技能目录(官方 `skill-catalog` 的注入)。**默认就按这个模式自己的工具名单推导**:`(allowTools 留空 ? 全部 : allowTools) − denyTools` 里含 `skill` 就要;写 `false` 就丢掉官方 `skill-catalog` 的注入(`skill` 工具的可见性仍归 `allowTools`)。",
85
93
  en: "Whether the skill catalog applies (the official `skill-catalog` injection). The default derives it from this mode's own tool lists: it applies when `(empty allowTools ? every tool : allowTools) − denyTools` contains `skill`; `false` drops the official `skill-catalog` injection (`skill` tool visibility still belongs to `allowTools`)."
86
94
  })),
95
+ dropSections: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).default([]).description(localized({
96
+ zh: "这个模式下要丢弃的 system prompt section:写 section 名(精确匹配)或以 `*` 结尾的前缀(如 `mcp-*`)。",
97
+ en: "System prompt sections to drop for this mode: exact names, or prefixes ending in `*`."
98
+ })),
87
99
  runtimeContext: _deepseek_ai_schemastery.default.boolean().default(true).description(localized({
88
100
  zh: "是否要动态快照(文件沙箱策略、审批策略)与时钟注入(`standard` 那类 preset 声明的 `time-context`)。",
89
101
  en: "Whether the runtime snapshot (sandbox and approval policy) and the clock injection (`time-context`, declared by presets such as `standard`) apply."
@@ -94,17 +106,14 @@ const modeSchema = _deepseek_ai_schemastery.default.object({
94
106
  }))
95
107
  });
96
108
  const Config = _deepseek_ai_schemastery.default.object({
109
+ modes: _deepseek_ai_schemastery.default.dict(modeSchema).default({}),
97
110
  default: _deepseek_ai_schemastery.default.string().required().description(localized({
98
111
  zh: "新会话(还没选过模式的会话)用哪个模式;必须是下面 `modes` 里的一个 id。",
99
112
  en: "Mode a session starts in before anyone picks one; must be an id in `modes`."
100
113
  })).volatile(),
101
- modes: _deepseek_ai_schemastery.default.dict(modeSchema).required().description(localized({
102
- zh: "模式清单:id → 定义(persona / 工具名单 / 生效的 policy 规则 / 角色)。改它对**已运行会话**不自动生效——重挂后新建的会话、或重新应用模式的会话才用新定义。",
103
- en: "Mode roster: id to definition (persona, tool lists, effective policy rules, role). Edits do not follow into already-running sessions; sessions created after the row is remounted use the new definition."
104
- })).volatile(),
105
114
  models: _deepseek_ai_schemastery.default.dict(modelSchema).default({}).hidden()
106
115
  });
107
- const POLICY_NAME_SET = new Set(require_shared.POLICY_NAMES);
116
+ const POLICY_NAME_SET = new Set(require_mode_config.POLICY_NAMES);
108
117
  function configProblem(config) {
109
118
  const roles = (id) => config.modes[id]?.role ?? ["main"];
110
119
  const ids = Object.keys(config.modes);
@@ -114,7 +123,7 @@ function configProblem(config) {
114
123
  if (roleless.length > 0) return `session-mode: mode(s) ${roleless.join(", ")} declare no \`role\`; declare main and/or subagent instead of leaving it empty`;
115
124
  if (!roles(config.default).includes("main")) return `session-mode: \`default\` names ${JSON.stringify(config.default)}, which does not declare role "main"; a session that can never be re-selected is a contradiction`;
116
125
  const unknownPolicies = Object.entries(config.modes).flatMap(([id, mode]) => [...mode.allowPolicies ?? [], ...mode.denyPolicies ?? []].filter((policy) => !POLICY_NAME_SET.has(policy)).map((policy) => `${id}: ${policy}`));
117
- if (unknownPolicies.length > 0) return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${require_shared.POLICY_NAMES.join(", ")}`;
126
+ if (unknownPolicies.length > 0) return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${require_mode_config.POLICY_NAMES.join(", ")}`;
118
127
  if (Object.keys(config.models ?? {}).length > 0) return "session-mode: `models` has moved into each mode's `defaultModel`; move the entries there and drop the top-level `models`";
119
128
  const retiredPresets = Object.entries(config.modes).filter(([, mode]) => mode.preset !== void 0).map(([id]) => id);
120
129
  if (retiredPresets.length > 0) return `session-mode: mode(s) ${retiredPresets.join(", ")} still declare \`preset\`; it has moved to \`presetsOnly\` (the presets that mode may ride on)`;
@@ -143,6 +152,137 @@ function installPersona(agent, persona) {
143
152
  for (const dispose of disposers) dispose();
144
153
  };
145
154
  }
155
+ // src/ptc-sdk.ts
156
+ function renderSkeleton(groups, members = []) {
157
+ const list = groups.length === 0 ? "(当前没有可用的组)" : groups.map((g) => JSON.stringify(g)).join(" | ");
158
+ return [
159
+ "在 `run_code` 写的程序里调用工具:`await tools.name(args)`;名字带引号的成员用索引访问(`tools[\"名字\"](args)`)。**常驻组**的签名已列在下面;其余组用 `describe_tool({ group })` 现取(不带 `group` 返回分组清单):",
160
+ "",
161
+ "```ts",
162
+ "declare global {",
163
+ ` interface ${_morlay_dsh_tool_guidance_groups.TOOLS_SDK_INTERFACE} {`,
164
+ ...members,
165
+ ` describe_tool(args: { group?: ${list} }): Promise<{ text?: string }>`,
166
+ " }",
167
+ ` const tools: ${_morlay_dsh_tool_guidance_groups.TOOLS_SDK_INTERFACE}`,
168
+ "}",
169
+ "```"
170
+ ].join("\n");
171
+ }
172
+ renderSkeleton([]);
173
+ function renderTools(schemas) {
174
+ if (schemas.length === 0) return "";
175
+ const sorted = [...schemas].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
176
+ const declaration = (0, _morlay_dsh_tool_guidance_groups.renderToolsDeclaration)(sorted.map((schema) => ({
177
+ name: schema.name,
178
+ args: schema.args,
179
+ output: schema.output
180
+ })));
181
+ const hint = sorted.some((schema) => (0, _morlay_dsh_tool_guidance_groups.renderToolKey)(schema.name) !== schema.name) ? "名字带引号的成员用索引访问:`tools[\"名字\"](args)`。\n\n" : "";
182
+ const sections = schemas.map((schema) => schema.section?.trim() ?? "").filter((text) => text.length > 0).join("\n\n");
183
+ return `${sections.length === 0 ? "" : `${sections}\n\n`}${hint}${[
184
+ "```ts",
185
+ declaration,
186
+ "```"
187
+ ].join("\n")}`;
188
+ }
189
+ function replaceToolsSdk(sections, skeleton) {
190
+ return sections.map((section) => section.name === "tools:sdk" ? {
191
+ ...section,
192
+ text: skeleton
193
+ } : section);
194
+ }
195
+ // src/ptc-describe.ts
196
+ function groupEntries(entries) {
197
+ const groups = /* @__PURE__ */ new Map();
198
+ for (const entry of entries) {
199
+ const group = require_mode_config.toolGroupOf(entry.name);
200
+ const list = groups.get(group);
201
+ const schema = {
202
+ name: entry.name,
203
+ description: _morlay_dsh_tool_guidance_groups.SHORT_TOOL_DESCRIPTIONS[entry.name] ?? entry.description,
204
+ args: schemaText(entry.parametersSchema),
205
+ output: schemaText(entry.outputSchema)
206
+ };
207
+ if (list === void 0) groups.set(group, [schema]);
208
+ else list.push(schema);
209
+ }
210
+ return groups;
211
+ }
212
+ function schemaText(schema) {
213
+ return schema === void 0 ? "JsonValue" : (0, _deepseek_ai_dsh_tool_executor_ptc.jsonSchemaToTs)(schema);
214
+ }
215
+ const GROUP_SUMMARY = {
216
+ fs: "文件读写",
217
+ shell: "执行命令",
218
+ "web-access": "联网检索与抓取",
219
+ ask: "向用户提问",
220
+ plan: "计划与待办",
221
+ swarm: "子代理与消息",
222
+ skill: "加载技能说明",
223
+ code: "代码执行(PTC)",
224
+ mcp: "MCP 资源",
225
+ onDemand: "按需工具",
226
+ other: "其它"
227
+ };
228
+ const GROUP_SECTION = {
229
+ collab: "## 协作\n\n可以派发子代理、给队友发消息、等待队友;所有人共享同一个工作目录与文件系统,写操作要划分互不重叠的范围,必要时用共享任务记录范围与依赖。",
230
+ "mcp:resources": "## MCP 资源\n\n按 URI 读取 MCP server 暴露的只读资源(文档、日志、数据);先 list 再 read。当前 server 都是 tools-only,通常没有可读资源。",
231
+ fs: "## 文件\n\n读、写、改文件与检索;改文件前先读当前内容,避免覆盖别人的改动。",
232
+ shell: "## Shell\n\n执行命令;不要用它绕过文件工具做批量改写。",
233
+ "web-access": "## 联网\n\n搜索与抓取网页。",
234
+ skill: "## 技能\n\n按需加载技能说明,再照着做。"
235
+ };
236
+ function sectionOfGroup(group) {
237
+ return GROUP_SECTION[group] ?? "";
238
+ }
239
+ function summaryOf(group) {
240
+ if (group.startsWith("mcp:")) return `MCP · ${group.slice(4)}`;
241
+ return GROUP_SUMMARY[group] ?? "其它";
242
+ }
243
+ function describeTools(entries, group) {
244
+ const groups = groupEntries(entries);
245
+ if (group === void 0) return [...groups.keys()].sort().map((name) => `${name} — ${summaryOf(name)}`).join("\n");
246
+ const declaration = renderTools(groups.get(group) ?? []);
247
+ const section = sectionOfGroup(group);
248
+ return section.length === 0 ? declaration : `${section}\n\n${declaration}`;
249
+ }
250
+ // src/per-agent.ts
251
+ function perAgentAccount(ctx, agent) {
252
+ const stoppers = [];
253
+ let released = false;
254
+ let rowAccount = () => {};
255
+ let agentAccount = () => {};
256
+ const release = () => {
257
+ if (released) return;
258
+ released = true;
259
+ for (const stop of stoppers.splice(0).reverse()) stop();
260
+ agentAccount();
261
+ rowAccount();
262
+ };
263
+ rowAccount = ctx.effect(() => () => release(), "session-mode: per-agent releases");
264
+ agentAccount = agent.ctx.effect(() => () => release(), "session-mode: per-agent releases");
265
+ return {
266
+ add: (stop) => {
267
+ if (released) stop();
268
+ else stoppers.push(stop);
269
+ },
270
+ release
271
+ };
272
+ }
273
+ // src/tool-groups.ts
274
+ function groupTools(names) {
275
+ const groups = /* @__PURE__ */ new Map();
276
+ for (const name of names) {
277
+ const group = require_mode_config.toolGroupOf(name);
278
+ if (require_mode_config.isResidentToolGroup(group)) continue;
279
+ const key = require_mode_config.groupRef(group);
280
+ const tools = groups.get(key);
281
+ if (tools === void 0) groups.set(key, [name]);
282
+ else tools.push(name);
283
+ }
284
+ return groups;
285
+ }
146
286
  // src/scope.ts
147
287
  const SKILL_OUT_OF_SCOPE = (skillName, modeName) => `${skillName} 不在「${modeName}」的技能白名单里,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
148
288
  const SKILL_DENYLISTED = (skillName, modeName) => `${skillName} 被「${modeName}」的技能黑名单排除,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
@@ -150,6 +290,7 @@ function skillRefusalOf(skillName, modeName, denial) {
150
290
  return denial === "denylisted" ? SKILL_DENYLISTED(skillName, modeName) : SKILL_OUT_OF_SCOPE(skillName, modeName);
151
291
  }
152
292
  const TOOL_SECTION_PREFIX = "tool:";
293
+ const RUNTIME_SECTIONS = /* @__PURE__ */ new Set(["harness:source", "app:web-surface"]);
153
294
  const SKILL_TOOL_NAME = "skill";
154
295
  function gateOfNames(allow, deny) {
155
296
  const denied = new Set(deny);
@@ -209,69 +350,221 @@ function skillNameOf(arguments_) {
209
350
  }
210
351
  var SessionScope = class {
211
352
  applied = /* @__PURE__ */ new WeakMap();
353
+ activatedGroups = /* @__PURE__ */ new WeakMap();
354
+ faces = /* @__PURE__ */ new WeakMap();
355
+ catalogs = /* @__PURE__ */ new WeakMap();
212
356
  ctx;
213
357
  constructor(ctx) {
214
358
  this.ctx = ctx;
215
359
  ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
216
360
  const agent = context.agent;
217
361
  const applied = agent === void 0 ? void 0 : this.applied.get(agent);
218
- if (applied === void 0 || applied.gate === void 0) return next();
362
+ if (applied === void 0) return next();
219
363
  const gate = applied.gate;
220
364
  const result = await next();
365
+ const filtered = result.sections.filter((section) => {
366
+ if (!applied.runtimeContext && RUNTIME_SECTIONS.has(section.name)) return false;
367
+ if (applied.dropSections.some((pattern) => pattern.endsWith("*") ? section.name.startsWith(pattern.slice(0, -1)) : section.name === pattern)) return false;
368
+ if (gate === void 0) return true;
369
+ if (gate !== void 0 && section.name.startsWith(TOOL_SECTION_PREFIX)) return denialOf(gate, section.name.slice(5)) === void 0;
370
+ if (gate !== void 0 && section.name.startsWith("mcp:")) {
371
+ const server = section.name.slice(4);
372
+ return applied.known.some((name) => name.startsWith(`mcp__${server}__`) && denialOf(gate, name) === void 0);
373
+ }
374
+ return true;
375
+ });
221
376
  return {
222
377
  ...result,
223
- sections: result.sections.filter((section) => section.name.startsWith(TOOL_SECTION_PREFIX) ? denialOf(gate, section.name.slice(5)) === void 0 : true)
378
+ sections: replaceToolsSdk([...filtered], renderSkeleton([...new Set(applied.known.map((name) => require_mode_config.toolGroupOf(name)))].sort()))
224
379
  };
225
380
  });
226
381
  }
382
+ account(agent) {
383
+ return perAgentAccount(this.ctx, agent);
384
+ }
227
385
  apply(agent, definition) {
228
386
  const previous = this.applied.get(agent);
229
387
  if (previous !== void 0) previous.dispose();
230
388
  const gate = gateOfNames(definition.allowTools, definition.denyTools);
231
389
  const skillGate = gateOfNames(definition.allowSkills, definition.denySkills);
232
- const dispose = agent.ctx.effect(() => {
233
- const stoppers = [];
234
- stoppers.push(agent.ctx.on("agent/pre-step", async (_payload, next) => {
235
- const decision = await next();
236
- if (decision.kind !== "enter") return decision;
237
- const kept = this.suppress(decision.messages, definition, skillGate);
238
- return kept === void 0 ? decision : {
239
- ...decision,
240
- messages: kept
241
- };
242
- }, { prepend: true }));
243
- if (definition.runtimeContext === false) stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
244
- const install = (scope) => {
245
- const disposers = [];
246
- if (gate !== void 0) {
247
- const known = new Set(scope.tools.definitions(agent).map((tool) => tool.name));
248
- disposers.push(scope.tools.restrict({
249
- ...gate.allow === void 0 ? {} : { allow: [...gate.allow].filter((name) => known.has(name)) },
250
- ...gate.deny.size === 0 ? {} : { deny: [...gate.deny].filter((name) => known.has(name)) }
251
- }));
252
- }
253
- if (skillGate === void 0) return disposers;
254
- disposers.push(scope.tools.guard((exec) => {
255
- if (exec.name !== SKILL_TOOL_NAME) return void 0;
256
- const skillName = skillNameOf(exec.arguments);
257
- if (skillName === void 0) return void 0;
258
- const skillDenial = denialOf(skillGate, skillName);
259
- return skillDenial === void 0 ? void 0 : skillRefusalOf(skillName, definition.name, skillDenial);
260
- }));
261
- return disposers;
390
+ const account = this.account(agent);
391
+ account.add(agent.ctx.on("agent/pre-step", async (_payload, next) => {
392
+ const decision = await next();
393
+ if (decision.kind !== "enter") return decision;
394
+ const kept = this.suppress(decision.messages, definition, skillGate);
395
+ return kept === void 0 ? decision : {
396
+ ...decision,
397
+ messages: kept
262
398
  };
263
- if (agent.ctx.get("tools") === void 0) {
264
- const fiber = agent.ctx.inject(["tools"], (scope) => {
265
- stoppers.push(...install(scope));
266
- });
267
- stoppers.push(() => fiber.dispose());
268
- } else stoppers.push(...install(agent.ctx));
269
- return stoppers;
270
- }, "session-mode: per-session gate");
399
+ }, { prepend: true }));
400
+ if (definition.runtimeContext === false) account.add(agent.ctx.systemPrompt.suppressRuntimeContext());
401
+ const install = (scope) => {
402
+ const disposers = [];
403
+ const definitions = scope.tools.definitions(agent);
404
+ const known = definitions.map((tool) => tool.name);
405
+ this.catalogs.set(agent, definitions.map((tool) => ({
406
+ name: tool.name,
407
+ description: tool.description
408
+ })));
409
+ const groups = gate === void 0 || gate.allow === void 0 ? groupTools(known) : /* @__PURE__ */ new Map();
410
+ const activated = this.activatedGroups.get(agent) ?? /* @__PURE__ */ new Set();
411
+ this.activatedGroups.set(agent, activated);
412
+ const face = {
413
+ agent,
414
+ known,
415
+ baseDeny: new Set(definition.denyTools),
416
+ allowOverride: new Set(definition.allowToolsOverride ?? []),
417
+ denyOverride: new Set(definition.denyToolsOverride ?? []),
418
+ groups
419
+ };
420
+ this.faces.set(agent, face);
421
+ disposers.push(scope.tools.register(this.toolsDescribeTool(scope)));
422
+ this.restrict(scope, gate, face, activated);
423
+ if (skillGate !== void 0) disposers.push(scope.tools.guard((exec) => {
424
+ if (exec.name !== SKILL_TOOL_NAME) return void 0;
425
+ const skillName = skillNameOf(exec.arguments);
426
+ if (skillName === void 0) return void 0;
427
+ const skillDenial = denialOf(skillGate, skillName);
428
+ return skillDenial === void 0 ? void 0 : skillRefusalOf(skillName, definition.name, skillDenial);
429
+ }));
430
+ return [...disposers, () => {
431
+ face.disposeRestriction?.();
432
+ face.disposeRestriction = void 0;
433
+ }];
434
+ };
435
+ if (agent.ctx.get("tools") === void 0) {
436
+ const fiber = agent.ctx.inject(["tools"], (scope) => {
437
+ for (const stop of install(scope)) account.add(stop);
438
+ });
439
+ account.add(() => {
440
+ fiber.dispose().catch((error) => this.ctx.logger.error(error));
441
+ });
442
+ } else for (const stop of install(agent.ctx)) account.add(stop);
271
443
  this.channel()?.setInstructions(agent, definition.instructions);
444
+ const known = this.faces.get(agent)?.known ?? [];
445
+ const expand = (names) => {
446
+ const out = /* @__PURE__ */ new Set();
447
+ for (const ref of names) if (ref.startsWith("group:") || ref.startsWith("mcp:")) {
448
+ const group = require_mode_config.refGroup(ref);
449
+ for (const tool of known) if (require_mode_config.toolGroupOf(tool) === group) out.add(tool);
450
+ } else out.add(ref);
451
+ return [...out];
452
+ };
453
+ const expandedDeny = expand(definition.denyTools);
454
+ for (const id of definition.allowToolsOverride ?? []) {
455
+ const at = expandedDeny.indexOf(id);
456
+ if (at >= 0) expandedDeny.splice(at, 1);
457
+ }
458
+ for (const id of definition.denyToolsOverride ?? []) if (!expandedDeny.includes(id)) expandedDeny.push(id);
459
+ const allowExpanded = expand(definition.allowTools);
272
460
  this.applied.set(agent, {
273
- gate,
274
- dispose
461
+ gate: gateOfNames(allowExpanded, [...expandedDeny, ...this.onDemandDeny(agent)]),
462
+ allowExpanded,
463
+ denyExpanded: expandedDeny,
464
+ known,
465
+ dropSections: definition.dropSections ?? [],
466
+ runtimeContext: definition.runtimeContext,
467
+ dispose: () => account.release()
468
+ });
469
+ }
470
+ onDemandDeny(agent) {
471
+ const face = this.faces.get(agent);
472
+ if (face === void 0) return [];
473
+ const activated = this.activatedGroups.get(agent);
474
+ const denied = [];
475
+ for (const [group, tools] of face.groups) {
476
+ if (activated?.has(group) === true) continue;
477
+ denied.push(...tools);
478
+ }
479
+ return denied;
480
+ }
481
+ knownTools(agent) {
482
+ const applied = this.applied.get(agent);
483
+ if (applied === void 0) return [];
484
+ const names = (this.ctx.get("tools")?.definitions(agent) ?? applied.known.map((name) => ({ name }))).map((definition) => definition.name);
485
+ const gate = applied.gate;
486
+ return gate === void 0 ? names : names.filter((tool) => denialOf(gate, tool) === void 0);
487
+ }
488
+ catalogOf(agent) {
489
+ return this.catalogs.get(agent) ?? [];
490
+ }
491
+ restrict(scope, gate, face, activated) {
492
+ face.disposeRestriction?.();
493
+ face.disposeRestriction = void 0;
494
+ const denied = /* @__PURE__ */ new Set();
495
+ for (const ref of face.baseDeny) if (ref.startsWith("group:") || ref.startsWith("mcp:")) {
496
+ const group = require_mode_config.refGroup(ref);
497
+ for (const tool of face.known) if (require_mode_config.toolGroupOf(tool) === group) denied.add(tool);
498
+ } else denied.add(ref);
499
+ for (const [group, tools] of face.groups) {
500
+ if (activated.has(group)) continue;
501
+ for (const tool of tools) denied.add(tool);
502
+ }
503
+ for (const id of face.allowOverride) denied.delete(id);
504
+ for (const id of face.denyOverride) denied.add(id);
505
+ for (const id of denied) if (require_mode_config.isProtectedTool(id)) denied.delete(id);
506
+ if (gate?.allow === void 0 && denied.size === 0) return;
507
+ const known = new Set(face.known);
508
+ const allow = gate?.allow === void 0 ? void 0 : [...gate.allow, ...[...known].filter(require_mode_config.isProtectedTool)].filter((name) => known.has(name));
509
+ const denyList = [...denied].filter((name) => known.has(name));
510
+ face.disposeRestriction = this.installRestriction(scope, allow, denyList);
511
+ }
512
+ installRestriction(scope, allow, deny) {
513
+ const attempt = (nextAllow, nextDeny) => {
514
+ if (nextAllow === void 0 && nextDeny.length === 0) return void 0;
515
+ try {
516
+ return scope.tools.restrict({
517
+ ...nextAllow === void 0 ? {} : { allow: [...nextAllow] },
518
+ ...nextDeny.length === 0 ? {} : { deny: [...nextDeny] }
519
+ });
520
+ } catch (error) {
521
+ this.ctx.logger.debug(`session-mode: 工具面收窄降级(${String(error)})`);
522
+ return;
523
+ }
524
+ };
525
+ const first = attempt(allow, deny);
526
+ if (first !== void 0 || allow === void 0 && deny.length === 0) return first;
527
+ const global = this.globalToolNames();
528
+ return attempt(allow === void 0 ? void 0 : allow.filter((name) => global.has(name)), deny.filter((name) => global.has(name)));
529
+ }
530
+ globalToolNames() {
531
+ const tools = this.ctx.get("tools");
532
+ return new Set((tools?.definitions() ?? []).map((definition) => definition.name));
533
+ }
534
+ toolsDescribeTool(scope) {
535
+ return (0, _deepseek_ai_dsh_tools.defineTool)({
536
+ name: "describe_tool",
537
+ description: "",
538
+ parameters: { group: { type: "string" } },
539
+ output: {
540
+ schema: {
541
+ type: "object",
542
+ additionalProperties: false,
543
+ properties: { text: {
544
+ type: "string",
545
+ required: true
546
+ } }
547
+ },
548
+ render: (_args, value) => [{
549
+ type: "text",
550
+ text: value.text
551
+ }]
552
+ },
553
+ async execute(args) {
554
+ const definitions = scope.tools.definitions();
555
+ const prompt = scope.get("systemPrompt");
556
+ const sectionOf = (name) => {
557
+ const section = prompt?.sections?.get(`tool:${name}`);
558
+ return typeof section?.text === "string" ? section.text : void 0;
559
+ };
560
+ return { text: describeTools(definitions.map((definition) => ({
561
+ name: definition.name,
562
+ description: definition.description ?? "",
563
+ parametersSchema: (0, _deepseek_ai_dsh_tools.projectToolSchema)(definition, false).parameters,
564
+ outputSchema: definition.output?.schema,
565
+ section: sectionOf(definition.name)
566
+ })), typeof args.group === "string" ? args.group : void 0) };
567
+ }
275
568
  });
276
569
  }
277
570
  suppress(messages, definition, skillGate) {
@@ -314,6 +607,49 @@ var SessionScope = class {
314
607
  }
315
608
  }
316
609
  };
610
+ // src/mode-store.ts
611
+ function modesDir() {
612
+ return (0, _deepseek_ai_dsh_home_paths.dshHomePath)("modes");
613
+ }
614
+ async function loadModeOverrides() {
615
+ const dir = modesDir();
616
+ const entries = await (0, node_fs_promises.readdir)(dir).catch(() => []);
617
+ const overrides = {};
618
+ for (const name of entries) {
619
+ if (!name.endsWith(".toml")) continue;
620
+ const text = await (0, node_fs_promises.readFile)((0, node_path.join)(dir, name), "utf8").catch(() => void 0);
621
+ if (text === void 0) continue;
622
+ try {
623
+ overrides[name.slice(0, -5)] = (0, smol_toml.parse)(text);
624
+ } catch {}
625
+ }
626
+ return overrides;
627
+ }
628
+ function prettyToml(text) {
629
+ return text.split("\n").map((line) => {
630
+ const match = /^(\s*[A-Za-z0-9_.-]+)\s*=\s*"((?:[^"\\]|\\.)*)"$/u.exec(line);
631
+ if (match === null) return line;
632
+ const [, key, raw] = match;
633
+ if (key === void 0 || raw === void 0 || !raw.includes("\\n")) return line;
634
+ return `${key} = """\n${raw.replace(/\\n/gu, "\n").replace(/\\"/gu, "\"").replace(/"""/gu, "\\\"\"\"")}"""`;
635
+ }).join("\n");
636
+ }
637
+ async function writeMode(id, value) {
638
+ await (0, node_fs_promises.mkdir)(modesDir(), { recursive: true });
639
+ await (0, node_fs_promises.writeFile)((0, node_path.join)(modesDir(), `${id}.toml`), prettyToml((0, smol_toml.stringify)(value)), "utf8");
640
+ }
641
+ async function removeMode(id) {
642
+ await (0, node_fs_promises.rm)((0, node_path.join)(modesDir(), `${id}.toml`), { force: true });
643
+ }
644
+ // src/session-mode-store.ts
645
+ const sessionModeRecord = zod.z.object({ config: zod.z.record(zod.z.string(), zod.z.unknown()) });
646
+ const sessionModeDomainSpec = (0, _deepseek_ai_dsh_storage_domain.defineDomain)({
647
+ name: "session_mode",
648
+ version: 1,
649
+ invalidRecords: "backup-and-skip",
650
+ layout: "per-record",
651
+ tables: { sessions: (0, _deepseek_ai_dsh_storage_domain.domainTable)(sessionModeRecord) }
652
+ });
317
653
  // src/index.ts
318
654
  const name = "session-mode";
319
655
  const inject = [
@@ -322,6 +658,9 @@ const inject = [
322
658
  "sessionProjections",
323
659
  "systemPrompt"
324
660
  ];
661
+ function plainMode(value) {
662
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
663
+ }
325
664
  const sessionModeSchema = zod.z.union([zod.z.string(), zod.z.null()]);
326
665
  const sessionModeProjection = {
327
666
  key: "sessionMode",
@@ -348,6 +687,8 @@ const sessionModeEditableProjection = {
348
687
  };
349
688
  var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
350
689
  installs = /* @__PURE__ */ new WeakMap();
690
+ modeOverrides = /* @__PURE__ */ new WeakMap();
691
+ sessionsTable;
351
692
  scope;
352
693
  projectionHandle;
353
694
  editableHandle;
@@ -358,7 +699,7 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
358
699
  super(ctx, "sessionModes");
359
700
  this.config = {
360
701
  default: config.default.get(),
361
- modes: structuredClone(config.modes.get())
702
+ modes: structuredClone(config.modes ?? {})
362
703
  };
363
704
  const problem = configProblem({
364
705
  ...this.config,
@@ -368,6 +709,7 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
368
709
  this.scope = new SessionScope(ctx);
369
710
  ctx.on("fs/write-intent", (_target, actor, next) => this.decidePolicy("fs/write-intent", actor, next), { prepend: true });
370
711
  ctx.on("fs/edit-intent", (_target, actor, next) => this.decidePolicy("fs/edit-intent", actor, next), { prepend: true });
712
+ ctx.on("fs/delete-intent", (_target, actor, next) => this.decidePolicy("fs/delete-intent", actor, next), { prepend: true });
371
713
  ctx.on("agent/created", ({ agent }) => {
372
714
  this.installFor(agent);
373
715
  });
@@ -380,11 +722,30 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
380
722
  });
381
723
  }
382
724
  static async create(ctx, config) {
383
- const modes = new SessionModes(ctx, config);
384
- await modes.init();
385
- return modes;
725
+ const injected = config.modes ?? {};
726
+ const modes = Object.keys(injected).length === 0 ? Object.fromEntries(require_rows.MODE_SOURCES.map((source) => [source.id, normalizeMode(require_rows.modeConfig(source))])) : injected;
727
+ const sessionModes = new SessionModes(ctx, {
728
+ ...config,
729
+ modes
730
+ });
731
+ await sessionModes.init();
732
+ return sessionModes;
386
733
  }
387
734
  async init() {
735
+ const modes = this.config.modes;
736
+ for (const [id, value] of Object.entries(await loadModeOverrides())) {
737
+ if (id === "noop") continue;
738
+ modes[id] = normalizeMode({
739
+ ...modes[id],
740
+ ...value
741
+ });
742
+ }
743
+ const storage = this.ctx.get("storageDomain");
744
+ if (storage !== void 0) {
745
+ const domain = await storage.open(sessionModeDomainSpec);
746
+ this.ctx.effect(() => () => domain.close(), "session-mode.domainClose");
747
+ this.sessionsTable = domain.table("sessions");
748
+ }
388
749
  this.projectionHandle = await this.ctx.sessionProjections.register(sessionModeProjection);
389
750
  this.editableHandle = await this.ctx.sessionProjections.register(sessionModeEditableProjection);
390
751
  this.modelSelectionHandle = await this.ctx.sessionProjections.register(_deepseek_ai_dsh_agent.modelSelectionProjectionDefinition);
@@ -433,9 +794,254 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
433
794
  editableOf(session) {
434
795
  return this.editableHandle.stateOf(session);
435
796
  }
797
+ buildCatalog(agent, base, merged, registered, visible) {
798
+ const entry = (id, description, group, allowKey, denyKey) => {
799
+ const fallback = require_mode_config.deniedByList(base[denyKey], id) ? "deny" : base[allowKey].length > 0 ? base[allowKey].includes(id) ? "allow" : "deny" : "default";
800
+ return {
801
+ id,
802
+ description,
803
+ ...group === void 0 ? {} : {
804
+ group,
805
+ groupLabel: require_mode_config.toolGroupLabel(group)
806
+ },
807
+ fallback,
808
+ override: (() => {
809
+ const baseDeny = require_mode_config.deniedByList(base[denyKey], id);
810
+ const effDeny = require_mode_config.deniedByList(merged[denyKey], id);
811
+ if (baseDeny !== effDeny) return effDeny ? "deny" : "allow";
812
+ const baseAllow = base[allowKey].includes(id);
813
+ const effAllow = merged[allowKey].includes(id);
814
+ if (baseAllow !== effAllow) return effAllow ? "allow" : "deny";
815
+ return null;
816
+ })()
817
+ };
818
+ };
819
+ const known = /* @__PURE__ */ new Map();
820
+ if (agent !== void 0) {
821
+ for (const tool of this.scope.catalogOf(agent)) known.set(tool.name, tool);
822
+ for (const tool of visible) if (!known.has(tool.name)) known.set(tool.name, {
823
+ name: tool.name,
824
+ description: tool.description ?? ""
825
+ });
826
+ }
827
+ const tools = [...known.values()].map((tool) => entry(tool.name, tool.description, require_mode_config.toolGroupOf(tool.name), "allowTools", "denyTools"));
828
+ const toolIds = new Set(tools.map((tool) => tool.id));
829
+ const fsActive = [
830
+ "read",
831
+ "write",
832
+ "edit",
833
+ "apply_patch"
834
+ ].some((id) => toolIds.has(id));
835
+ return {
836
+ tools,
837
+ skills: registered.map((skill) => entry(skill.name, skill.description, void 0, "allowSkills", "denySkills")),
838
+ policies: require_mode_config.POLICY_NAMES.map((policy) => {
839
+ const row = entry(policy, require_mode_config.POLICY_DESCRIPTIONS[policy], void 0, "allowPolicies", "denyPolicies");
840
+ return fsActive ? row : {
841
+ ...row,
842
+ fallback: "deny",
843
+ override: null
844
+ };
845
+ })
846
+ };
847
+ }
848
+ knownToolsOf(agent) {
849
+ return this.scope.knownTools(agent);
850
+ }
436
851
  modeOfSession(session) {
437
852
  return this.definition(this.modeOf(session));
438
853
  }
854
+ record(agent) {
855
+ const session = this.modeOverrides.get(agent);
856
+ if (this.sessionsTable === void 0) return;
857
+ if (session === void 0) this.sessionsTable.delete(agent.session.id);
858
+ else this.sessionsTable.put(agent.session.id, { config: { ...session } });
859
+ }
860
+ hydrate(agent) {
861
+ if (this.modeOverrides.has(agent) || this.sessionsTable === void 0) return;
862
+ const row = this.sessionsTable.get(agent.session.id);
863
+ if (row === void 0) return;
864
+ this.modeOverrides.set(agent, row.config);
865
+ const modeId = this.resolveModeId(agent);
866
+ this.installs.get(agent)?.dispose();
867
+ this.installs.delete(agent);
868
+ this.installFor(agent, modeId);
869
+ }
870
+ async saveConfig(sessionId, config) {
871
+ const agent = this.ctx.agents.get(sessionId);
872
+ if (agent === void 0) return void 0;
873
+ const modeId = this.modeOf(agent.session);
874
+ this.config.modes[modeId] = config;
875
+ await writeMode(modeId, plainMode({ ...config }));
876
+ this.modeOverrides.delete(agent);
877
+ this.record(agent);
878
+ this.installs.get(agent)?.dispose();
879
+ this.installs.delete(agent);
880
+ this.installFor(agent, modeId);
881
+ return this.activeState(sessionId);
882
+ }
883
+ async applyConfig(sessionId, config) {
884
+ const agent = this.ctx.agents.get(sessionId);
885
+ if (agent === void 0) return void 0;
886
+ this.modeOverrides.set(agent, structuredClone(config));
887
+ this.record(agent);
888
+ const modeId = this.resolveModeId(agent);
889
+ this.installs.get(agent)?.dispose();
890
+ this.installs.delete(agent);
891
+ this.installFor(agent, modeId);
892
+ return this.activeState(sessionId);
893
+ }
894
+ toolNames(agent) {
895
+ const tools = this.ctx.get("tools");
896
+ const known = new Set((tools?.definitions() ?? []).map((tool) => tool.name));
897
+ for (const tool of this.scope.catalogOf(agent)) known.add(tool.name);
898
+ return [...known];
899
+ }
900
+ editableFaceOf(mode) {
901
+ return {
902
+ persona: { ...mode.persona },
903
+ allowTools: [...mode.allowTools],
904
+ denyTools: [...mode.denyTools],
905
+ allowSkills: [...mode.allowSkills],
906
+ denySkills: [...mode.denySkills],
907
+ allowPolicies: [...mode.allowPolicies],
908
+ denyPolicies: [...mode.denyPolicies],
909
+ instructions: mode.instructions,
910
+ skills: mode.skills ?? true,
911
+ runtimeContext: mode.runtimeContext
912
+ };
913
+ }
914
+ definitionFor(agent, modeId) {
915
+ const base = this.definition(modeId);
916
+ const normalized = require_mode_config.asModeConfig(base, base.skills ?? true);
917
+ const session = this.modeOverrides.get(agent);
918
+ return require_mode_config.resolveModeConfig(normalized, session === void 0 ? void 0 : {
919
+ ...normalized,
920
+ ...session
921
+ });
922
+ }
923
+ overriddenKeys(agent) {
924
+ if (agent === void 0) return [];
925
+ const override = this.modeOverrides.get(agent);
926
+ if (override === void 0) return [];
927
+ return Object.keys(override).filter((key) => override[key] !== void 0);
928
+ }
929
+ overriddenFields(agent) {
930
+ if (agent === void 0) return [];
931
+ const override = this.modeOverrides.get(agent);
932
+ return override === void 0 ? [] : Object.keys(override);
933
+ }
934
+ async setModePatch(sessionId, patch) {
935
+ const agent = this.ctx.agents.get(sessionId);
936
+ if (agent === void 0) return void 0;
937
+ const next = { ...this.modeOverrides.get(agent) };
938
+ for (const [key, value] of Object.entries(patch)) if (value === null) Reflect.deleteProperty(next, key);
939
+ else next[key] = value;
940
+ this.modeOverrides.set(agent, next);
941
+ const modeId = this.resolveModeId(agent);
942
+ this.record(agent);
943
+ this.installs.get(agent)?.dispose();
944
+ this.installs.delete(agent);
945
+ this.installFor(agent, modeId);
946
+ return this.activeState(sessionId);
947
+ }
948
+ async setSwitches(sessionId, patch) {
949
+ if (this.ctx.agents.get(sessionId) === void 0) return void 0;
950
+ return this.setModePatch(sessionId, patch);
951
+ }
952
+ async saveAsDefault(sessionId) {
953
+ const agent = this.ctx.agents.get(sessionId);
954
+ if (agent === void 0) throw new Error("会话不存在。");
955
+ if (this.modeOverrides.get(agent) === void 0) return;
956
+ const modeId = this.modeOf(agent.session);
957
+ const editable = this.editableFaceOf(this.definitionFor(agent, modeId));
958
+ const next = plainMode({
959
+ ...this.config.modes[modeId],
960
+ ...editable
961
+ });
962
+ this.config.modes[modeId] = next;
963
+ await writeMode(modeId, next);
964
+ this.modeOverrides.delete(agent);
965
+ this.installs.get(agent)?.dispose();
966
+ this.installs.delete(agent);
967
+ this.installFor(agent, modeId);
968
+ return this.activeState(sessionId);
969
+ }
970
+ async saveAsNewMode(sessionId, code, name) {
971
+ const agent = this.ctx.agents.get(sessionId);
972
+ if (agent === void 0) throw new Error("会话不存在。");
973
+ const id = code.trim();
974
+ if (id.length === 0) throw new Error("模式 code 不能为空。");
975
+ if (!/^[a-zA-Z0-9_-]+$/u.test(id)) throw new Error("模式 code 只能包含字母、数字、下划线与连字符。");
976
+ if (this.config.modes[id] !== void 0) throw new Error(`模式 ${id} 已存在。`);
977
+ const next = plainMode({
978
+ ...this.definitionFor(agent, this.resolveModeId(agent)),
979
+ name: name?.trim() || id
980
+ });
981
+ this.config.modes[id] = next;
982
+ await writeMode(id, next);
983
+ return id;
984
+ }
985
+ async deleteMode(sessionId) {
986
+ const agent = this.ctx.agents.get(sessionId);
987
+ if (agent === void 0) throw new Error("会话不存在。");
988
+ const modeId = this.modeOf(agent.session);
989
+ if (modeId === this.defaultId) throw new Error("部署默认模式不能删除。");
990
+ const remaining = Object.keys(this.config.modes).filter((id) => id !== modeId);
991
+ if (remaining.length === 0) throw new Error("至少要留一个模式。");
992
+ const fallback = this.defaultId === modeId ? remaining[0] : this.defaultId;
993
+ Reflect.deleteProperty(this.config.modes, modeId);
994
+ await removeMode(modeId);
995
+ this.modeOverrides.delete(agent);
996
+ if (this.projectionHandle.stateOf(agent.session) === modeId) agent.session.append("session-mode/selected", { sessionMode: fallback });
997
+ this.installs.get(agent)?.dispose();
998
+ this.installFor(agent, fallback);
999
+ return fallback;
1000
+ }
1001
+ async activeState(sessionId) {
1002
+ const session = this.ctx.sessions.get(sessionId);
1003
+ if (session === void 0) return void 0;
1004
+ const agent = this.ctx.agents.get(sessionId);
1005
+ const id = this.modeOf(session);
1006
+ const mode = agent === void 0 ? this.definition(id) : this.definitionFor(agent, id);
1007
+ const unwrap = (value) => value === void 0 ? void 0 : value[Symbol.for("cordis.original")] ?? value;
1008
+ const tools = unwrap(this.ctx.get("tools"));
1009
+ const skills = unwrap(this.ctx.get("skills"));
1010
+ const cwd = (agent?.session.header)?.cwd;
1011
+ const registered = skills === void 0 ? [] : await skills.list({
1012
+ ...agent === void 0 ? {} : { scope: agent },
1013
+ ...typeof cwd === "string" ? { cwd } : {}
1014
+ }).catch((error) => {
1015
+ this.ctx.logger.warn("session-mode: skills.list() failed %o", { error: String(error) });
1016
+ return [];
1017
+ });
1018
+ const config = require_mode_config.asModeConfig(this.definition(id), this.definition(id).skills ?? true);
1019
+ const override = agent === void 0 ? void 0 : this.modeOverrides.get(agent);
1020
+ const overriddenFields = override === void 0 ? [] : [
1021
+ ["persona", "persona"],
1022
+ ["instructions", "instructions"],
1023
+ ["skills", "skills"],
1024
+ ["runtimeContext", "runtimeContext"],
1025
+ ["allowTools", "allowTools"],
1026
+ ["denyTools", "denyTools"],
1027
+ ["allowSkills", "allowSkills"],
1028
+ ["denySkills", "denySkills"],
1029
+ ["allowPolicies", "allowPolicies"],
1030
+ ["denyPolicies", "denyPolicies"]
1031
+ ].filter(([key]) => JSON.stringify(override[key]) !== JSON.stringify(config[key])).map(([, field]) => field);
1032
+ return {
1033
+ mode: {
1034
+ id,
1035
+ name: mode.name,
1036
+ description: mode.description
1037
+ },
1038
+ config,
1039
+ session: override,
1040
+ overriddenFields,
1041
+ deletable: id !== this.defaultId && Object.keys(this.config.modes).length > 1,
1042
+ catalog: this.buildCatalog(agent, this.definition(id), mode, registered, tools === void 0 ? [] : [...tools.definitions(), ...agent === void 0 ? [] : tools.schemas(agent)])
1043
+ };
1044
+ }
439
1045
  policyInForce(policy, actor) {
440
1046
  const agent = actor?.agent;
441
1047
  if (agent === void 0) return true;
@@ -459,7 +1065,7 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
459
1065
  const agent = this.ctx.agents.get(sessionId);
460
1066
  const registry = this.presetRegistry();
461
1067
  if (agent !== void 0 && registry !== void 0) {
462
- const target = this.presetTarget(definition.presetsOnly, this.presetOfAgent(registry, agent));
1068
+ const target = this.presetTarget(definition.presetsOnly ?? [], this.presetOfAgent(registry, agent));
463
1069
  if (target !== void 0) await registry.select(agent, target);
464
1070
  }
465
1071
  if (this.projectionHandle.stateOf(session) !== mode) session.append("session-mode/selected", { sessionMode: mode });
@@ -489,6 +1095,7 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
489
1095
  }
490
1096
  }
491
1097
  applyTo(agent, mode, options = {}) {
1098
+ this.hydrate(agent);
492
1099
  this.definition(mode);
493
1100
  if (options.record !== false) agent.session.append("session-mode/selected", { sessionMode: mode });
494
1101
  this.installFor(agent, mode);
@@ -512,7 +1119,7 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
512
1119
  }
513
1120
  modeForPreset(preset) {
514
1121
  if (preset === void 0) return void 0;
515
- const owners = Object.keys(this.config.modes).filter((id) => this.config.modes[id]?.presetsOnly.includes(preset));
1122
+ const owners = Object.keys(this.config.modes).filter((id) => (this.config.modes[id]?.presetsOnly ?? []).includes(preset));
516
1123
  return owners.length === 1 ? owners[0] : void 0;
517
1124
  }
518
1125
  inheritedModeId(agent) {
@@ -525,23 +1132,26 @@ var SessionModes = class SessionModes extends _deepseek_ai_cordis.Service {
525
1132
  const installed = this.installs.get(agent);
526
1133
  if (installed?.mode === modeId) return;
527
1134
  installed?.dispose();
528
- const mode = this.definition(modeId);
529
- const disposers = [installPersona(agent, mode.persona), this.installDefaultModel(agent, modeId)];
1135
+ const mode = this.definitionFor(agent, modeId);
1136
+ const account = this.scope.account(agent);
1137
+ account.add(installPersona(agent, mode.persona));
1138
+ account.add(this.installDefaultModel(agent, modeId));
530
1139
  this.installs.set(agent, {
531
1140
  mode: modeId,
532
- dispose: () => {
533
- for (const dispose of disposers) dispose();
534
- }
1141
+ dispose: () => account.release()
535
1142
  });
536
1143
  this.scope.apply(agent, {
537
1144
  name: mode.name,
538
1145
  allowTools: mode.allowTools,
539
1146
  denyTools: mode.denyTools,
1147
+ allowToolsOverride: [],
1148
+ denyToolsOverride: [],
540
1149
  allowSkills: mode.allowSkills,
541
1150
  denySkills: mode.denySkills,
542
1151
  instructions: mode.instructions,
543
1152
  skills: mode.skills ?? derivedSkills(mode),
544
- runtimeContext: mode.runtimeContext
1153
+ runtimeContext: mode.runtimeContext,
1154
+ dropSections: mode.dropSections ?? []
545
1155
  });
546
1156
  }
547
1157
  installDefaultModel(agent, modeId) {
@@ -585,20 +1195,87 @@ function requestJson(request) {
585
1195
  request.on("error", reject);
586
1196
  });
587
1197
  }
1198
+ function queryValue(url, name) {
1199
+ if (url === void 0) return void 0;
1200
+ const separator = url.indexOf("?");
1201
+ if (separator < 0) return void 0;
1202
+ const value = new URLSearchParams(url.slice(separator + 1)).get(name);
1203
+ return value === null || value.length === 0 ? void 0 : value;
1204
+ }
588
1205
  function requiredString(value, field) {
589
1206
  if (typeof value !== "string" || value.length === 0) throw new TypeError(`${field} 必须是非空字符串。`);
590
1207
  return value;
591
1208
  }
1209
+ function isWriteRequest(record) {
1210
+ return typeof record["switches"] === "object" || typeof record["modePatch"] === "object" || typeof record["itemState"] === "object" || typeof record["applyConfig"] === "object" || typeof record["saveConfig"] === "object" || typeof record["saveAsNewMode"] === "object" && record["saveAsNewMode"] !== null || record["deleteMode"] === true || record["saveAsDefault"] === true;
1211
+ }
592
1212
  async function handleRoute(modes, request, response) {
593
1213
  try {
594
1214
  if (request.method === "GET") {
595
- respondJson(response, 200, modes.roster());
1215
+ const sessionId = queryValue(request.url, "sessionId");
1216
+ if (sessionId === void 0) {
1217
+ respondJson(response, 200, modes.roster());
1218
+ return;
1219
+ }
1220
+ const active = await modes.activeState(sessionId);
1221
+ if (active === void 0) {
1222
+ respondJson(response, 404, { error: "会话不存在。" });
1223
+ return;
1224
+ }
1225
+ respondJson(response, 200, active);
596
1226
  return;
597
1227
  }
598
1228
  if (request.method === "POST") {
599
1229
  const body = await requestJson(request);
600
1230
  if (typeof body !== "object" || body === null || Array.isArray(body)) throw new TypeError("请求体必须是 JSON 对象。");
601
1231
  const record = body;
1232
+ if (isWriteRequest(record)) {
1233
+ const target = requiredString(record["sessionId"], "sessionId");
1234
+ const saved = record["saveAsDefault"] === true ? await modes.saveAsDefault(target) : void 0;
1235
+ if (saved !== void 0) {
1236
+ respondJson(response, 200, saved);
1237
+ return;
1238
+ }
1239
+ if (record["deleteMode"] === true) {
1240
+ await modes.deleteMode(target);
1241
+ const state = await modes.activeState(target);
1242
+ respondJson(response, state === void 0 ? 404 : 200, state ?? { error: "会话不存在。" });
1243
+ return;
1244
+ }
1245
+ const saving = record["saveAsNewMode"];
1246
+ if (typeof saving === "object" && saving !== null) {
1247
+ const value = saving;
1248
+ await modes.saveAsNewMode(target, requiredString(value.code, "code"), typeof value.name === "string" ? value.name : void 0);
1249
+ }
1250
+ if (typeof record["saveConfig"] === "object" && record["saveConfig"] !== null) {
1251
+ const draft = record["saveConfig"];
1252
+ if (draft.config === void 0) {
1253
+ respondJson(response, 400, { error: "saveConfig 需要完整的 config。" });
1254
+ return;
1255
+ }
1256
+ const state = await modes.saveConfig(target, draft.config);
1257
+ respondJson(response, state === void 0 ? 404 : 200, state ?? { error: "会话不存在。" });
1258
+ return;
1259
+ }
1260
+ if (typeof record["applyConfig"] === "object" && record["applyConfig"] !== null) {
1261
+ const draft = record["applyConfig"];
1262
+ if (draft.config === void 0) {
1263
+ respondJson(response, 400, { error: "applyConfig 需要完整的 config。" });
1264
+ return;
1265
+ }
1266
+ const state = await modes.applyConfig(target, draft.config);
1267
+ respondJson(response, state === void 0 ? 404 : 200, state ?? { error: "会话不存在。" });
1268
+ return;
1269
+ }
1270
+ const patch = record["modePatch"] ?? record["switches"];
1271
+ const state = typeof patch === "object" && patch !== null ? await modes.setModePatch(target, patch) : await modes.activeState(target);
1272
+ if (state === void 0) {
1273
+ respondJson(response, 404, { error: "会话不存在。" });
1274
+ return;
1275
+ }
1276
+ respondJson(response, 200, state);
1277
+ return;
1278
+ }
602
1279
  const sessionId = requiredString(record["sessionId"], "sessionId");
603
1280
  const mode = requiredString(record["mode"], "mode");
604
1281
  respondJson(response, 200, { mode: await modes.select(sessionId, mode) });
@@ -617,17 +1294,18 @@ function registerHttpRoutes(ctx, modes) {
617
1294
  if (webServer === void 0) return;
618
1295
  scope.effect(() => webServer.register({
619
1296
  kind: "exact",
620
- path: require_shared.SESSION_MODE_PATH,
1297
+ path: require_mode_config.SESSION_MODE_PATH,
621
1298
  handler: (request, response) => handleRoute(modes, request, response)
622
1299
  }), "session-mode: HTTP route");
623
1300
  });
624
1301
  }
625
1302
  exports.Config = Config;
626
- exports.POLICY_NAMES = require_shared.POLICY_NAMES;
627
- exports.SESSION_MODE_PATH = require_shared.SESSION_MODE_PATH;
1303
+ exports.POLICY_NAMES = require_mode_config.POLICY_NAMES;
1304
+ exports.SESSION_MODE_PATH = require_mode_config.SESSION_MODE_PATH;
628
1305
  exports.SessionModes = SessionModes;
629
1306
  exports.apply = apply;
630
1307
  exports.inject = inject;
1308
+ exports.isWriteRequest = isWriteRequest;
631
1309
  exports.name = name;
632
1310
  exports.sessionModeEditableProjection = sessionModeEditableProjection;
633
1311
  exports.sessionModeProjection = sessionModeProjection;