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