@morlay/dsh-session-mode 0.0.8 → 0.0.9
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 +7 -11
- package/dist/catalog-DWxIg3qw.mjs +131 -0
- package/dist/catalog-K5cESbqA.cjs +136 -0
- package/dist/catalog-remote-B7ZgYqun.cjs +28 -0
- package/dist/{catalog-remote-B-NMi_vS.mjs → catalog-remote-KXJjDaN6.mjs} +1 -5
- package/dist/client.cjs +61 -16357
- package/dist/client.d.cts +3 -326
- package/dist/client.d.mts +3 -326
- package/dist/client.mjs +1612 -0
- package/dist/index.cjs +635 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +45 -169
- package/dist/rows.cjs +85 -0
- package/dist/rows.mjs +2 -5
- package/dist/shared-DX2Giyh3.mjs +5 -0
- package/dist/shared-sy45ixVR.cjs +16 -0
- package/dist/testing.cjs +6 -0
- package/dist/testing.d.mts +35 -0
- package/dist/testing.mjs +3 -0
- package/package.json +72 -60
- package/src/client/BundleConfigPage.tsx +12 -11
- package/src/client/SessionModeSeat.tsx +1 -1
- package/src/client/bundle-config.ts +1 -1
- package/src/client/bundle-locales.ts +4 -2
- package/src/client/index.ts +8 -12
- package/src/client/llm-directory.ts +7 -4
- package/src/index.ts +4 -1
- package/src/mode-sources.ts +11 -8
- package/src/modes.ts +6 -5
- package/src/scope.ts +74 -65
- package/src/testing.ts +3 -0
- package/dist/index.d.cts +0 -146
- package/dist/rows.d.cts +0 -11
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
const require_catalog = require("./catalog-K5cESbqA.cjs");
|
|
25
|
+
const require_shared = require("./shared-sy45ixVR.cjs");
|
|
26
|
+
let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
|
|
27
|
+
let _deepseek_ai_dsh_llm = require("@deepseek-ai/dsh-llm");
|
|
28
|
+
let zod = require("zod");
|
|
29
|
+
let _deepseek_ai_schemastery = require("@deepseek-ai/schemastery");
|
|
30
|
+
_deepseek_ai_schemastery = __toESM(_deepseek_ai_schemastery, 1);
|
|
31
|
+
let _deepseek_ai_dsh_system_prompt = require("@deepseek-ai/dsh-system-prompt");
|
|
32
|
+
//#region src/modes.ts
|
|
33
|
+
function derivedSkills(mode) {
|
|
34
|
+
return (mode.allowTools.length === 0 || mode.allowTools.includes("skill")) && !mode.denyTools.includes("skill");
|
|
35
|
+
}
|
|
36
|
+
const localized = (text) => text;
|
|
37
|
+
const personaSchema = _deepseek_ai_schemastery.default.object({
|
|
38
|
+
prefix: _deepseek_ai_schemastery.default.string().default(""),
|
|
39
|
+
suffix: _deepseek_ai_schemastery.default.string().default("")
|
|
40
|
+
});
|
|
41
|
+
const roleSchema = _deepseek_ai_schemastery.default.union([_deepseek_ai_schemastery.default.const("main"), _deepseek_ai_schemastery.default.const("subagent")]);
|
|
42
|
+
const modelSchema = _deepseek_ai_schemastery.default.object({
|
|
43
|
+
provider: _deepseek_ai_schemastery.default.string().role("select", { source: "llm-providers" }).description(localized({
|
|
44
|
+
zh: "服务商 id(部署里注册的任意路由,不论哪个适配器插件注册的)。",
|
|
45
|
+
en: "Provider id (any route registered in this deployment, whichever adapter plugin supplies it)."
|
|
46
|
+
})),
|
|
47
|
+
model: _deepseek_ai_schemastery.default.string().role("select", { source: "llm-models" }).description(localized({
|
|
48
|
+
zh: "模型 id。",
|
|
49
|
+
en: "Model id."
|
|
50
|
+
})),
|
|
51
|
+
reasoningEffort: _deepseek_ai_schemastery.default.string().description(localized({
|
|
52
|
+
zh: "思考档位;默认跟服务商自己的默认。",
|
|
53
|
+
en: "Reasoning effort; the default is the provider's own."
|
|
54
|
+
}))
|
|
55
|
+
});
|
|
56
|
+
const modeSchema = _deepseek_ai_schemastery.default.object({
|
|
57
|
+
preset: _deepseek_ai_schemastery.default.string().hidden(),
|
|
58
|
+
presetsOnly: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "agent-presets" }).default([]).description(localized({
|
|
59
|
+
zh: "这个模式只允许挂哪些 agent preset(它们的 id,官方或本部署自建的):行清单由挂着的那份提供,几个模式可以共享同一份名单。留空表示**允许全部**——哪份 preset 都行,选这个模式也不动会话当前挂着的那份;有值时当前的已经在名单里就不动,否则换成名单里的第一个。模式自己的提示词、工具收口与开关照常生效。",
|
|
60
|
+
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."
|
|
61
|
+
})),
|
|
62
|
+
name: _deepseek_ai_schemastery.default.string().required().description(localized({
|
|
63
|
+
zh: "模式名(事实文案;选择面归官方 roster)。",
|
|
64
|
+
en: "Mode name (fact copy; the picker belongs to the official roster)."
|
|
65
|
+
})),
|
|
66
|
+
description: _deepseek_ai_schemastery.default.string().default("").description(localized({
|
|
67
|
+
zh: "一句话说明。",
|
|
68
|
+
en: "One-line description."
|
|
69
|
+
})),
|
|
70
|
+
role: _deepseek_ai_schemastery.default.array(roleSchema).default(["main"]).description(localized({
|
|
71
|
+
zh: "谁可以用这个模式:`main`(会话选择器里可选)或 `subagent`(子代理继承)。",
|
|
72
|
+
en: "Who may use this mode: `main` (selectable in sessions) or `subagent` (inherited by subagents)."
|
|
73
|
+
})),
|
|
74
|
+
persona: personaSchema.default({}),
|
|
75
|
+
allowTools: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "catalog-tools" }).default([]).description(localized({
|
|
76
|
+
zh: "这个会话能用的工具;其余既不进目录,调用也被拒。**留空就是不设收窄**:用这个会话挂着的 preset 提供的全部工具。",
|
|
77
|
+
en: "Tools this session may use; everything else leaves the catalog and calls are refused. Leave it empty to narrow nothing: the session then uses every tool its preset provides."
|
|
78
|
+
})),
|
|
79
|
+
denyTools: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "catalog-tools" }).default([]).description(localized({
|
|
80
|
+
zh: "这个模式不用的工具(黑名单):从 `allowTools` 定下的那份里减掉(`allowTools` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
81
|
+
en: "Tools this mode must not use (deny list): subtracted from whatever `allowTools` settled on (with an empty `allowTools`, from everything). Empty denies nothing; a name in both lists is denied."
|
|
82
|
+
})),
|
|
83
|
+
allowSkills: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "catalog-skills" }).default([]).description(localized({
|
|
84
|
+
zh: "这个模式能用的技能;其余既不进技能目录,`skill` 工具加载它也被拒。**留空就是不设收窄**:技能注册表里有什么就用什么。",
|
|
85
|
+
en: "Skills this mode may use; everything else leaves the skill catalog and loading it through the `skill` tool is refused. Leave it empty to narrow nothing: the session then sees every skill the registry carries."
|
|
86
|
+
})),
|
|
87
|
+
denySkills: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "catalog-skills" }).default([]).description(localized({
|
|
88
|
+
zh: "这个模式不用的技能(黑名单):从 `allowSkills` 定下的那份里减掉(`allowSkills` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
89
|
+
en: "Skills this mode must not use (deny list): subtracted from whatever `allowSkills` settled on (with an empty `allowSkills`, from everything). Empty denies nothing; a name in both lists is denied."
|
|
90
|
+
})),
|
|
91
|
+
allowPolicies: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "policies" }).default([]).description(localized({
|
|
92
|
+
zh: "上游 policy 规则的生效白名单(`fs/write-intent` / `fs/edit-intent`):留空 = 全部规则照旧生效;有值 = 只有列出的生效,其余的被绕过。",
|
|
93
|
+
en: "Allow list of upstream policy rules that stay in force (`fs/write-intent` / `fs/edit-intent`): empty keeps every rule; when set, only the listed ones stay, and the others are bypassed."
|
|
94
|
+
})),
|
|
95
|
+
denyPolicies: _deepseek_ai_schemastery.default.array(_deepseek_ai_schemastery.default.string()).role("select", { source: "policies" }).default([]).description(localized({
|
|
96
|
+
zh: "上游 policy 规则的黑名单:列出的规则禁用(它的裁决连拒绝一起丢掉,调用按没有这条规则继续)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
97
|
+
en: "Deny list of upstream policy rules: the listed ones are disabled — their verdict (rejection included) is dropped and the call proceeds as if the rule were absent. Empty denies nothing; a name in both lists is denied."
|
|
98
|
+
})),
|
|
99
|
+
instructions: _deepseek_ai_schemastery.default.boolean().default(true).description(localized({
|
|
100
|
+
zh: "是否要 instruction 类注入(工作区指令、用法正文):`false` 时丢掉官方 `agent-instructions` 的注入,并关掉通道自己的降级注入。",
|
|
101
|
+
en: "Whether instruction-class injections apply (workspace instructions, guidance): `false` drops the official `agent-instructions` injection and turns off the channel's own demoted delivery."
|
|
102
|
+
})),
|
|
103
|
+
skills: _deepseek_ai_schemastery.default.boolean().description(localized({
|
|
104
|
+
zh: "是否要技能目录(官方 `skill-catalog` 的注入)。**默认就按这个模式自己的工具名单推导**:`(allowTools 留空 ? 全部 : allowTools) − denyTools` 里含 `skill` 就要;写 `false` 就丢掉官方 `skill-catalog` 的注入(`skill` 工具的可见性仍归 `allowTools`)。",
|
|
105
|
+
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`)."
|
|
106
|
+
})),
|
|
107
|
+
runtimeContext: _deepseek_ai_schemastery.default.boolean().default(true).description(localized({
|
|
108
|
+
zh: "是否要动态快照(文件沙箱策略、审批策略)与时钟注入(`standard` 那类 preset 声明的 `time-context`)。",
|
|
109
|
+
en: "Whether the runtime snapshot (sandbox and approval policy) and the clock injection (`time-context`, declared by presets such as `standard`) apply."
|
|
110
|
+
})),
|
|
111
|
+
defaultModel: modelSchema.default(null).description(localized({
|
|
112
|
+
zh: "这个模式的默认模型;不设就跟全局默认模型。只在会话还没有模型事实时接管。",
|
|
113
|
+
en: "Default model for this mode; empty follows the global default. Applies only while a session has no model fact yet."
|
|
114
|
+
}))
|
|
115
|
+
});
|
|
116
|
+
const Config = _deepseek_ai_schemastery.default.object({
|
|
117
|
+
default: _deepseek_ai_schemastery.default.string().required().description(localized({
|
|
118
|
+
zh: "新会话(还没选过模式的会话)用哪个模式;必须是下面 `modes` 里的一个 id。",
|
|
119
|
+
en: "Mode a session starts in before anyone picks one; must be an id in `modes`."
|
|
120
|
+
})).volatile(),
|
|
121
|
+
modes: _deepseek_ai_schemastery.default.dict(modeSchema).required().description(localized({
|
|
122
|
+
zh: "模式清单:id → 定义(persona / 工具名单 / 生效的 policy 规则 / 角色)。改它对**已运行会话**不自动生效——重挂后新建的会话、或重新应用模式的会话才用新定义。",
|
|
123
|
+
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."
|
|
124
|
+
})).volatile(),
|
|
125
|
+
models: _deepseek_ai_schemastery.default.dict(modelSchema).default({}).hidden()
|
|
126
|
+
});
|
|
127
|
+
const POLICY_NAME_SET = new Set(require_shared.POLICY_NAMES);
|
|
128
|
+
function configProblem(config) {
|
|
129
|
+
const roles = (id) => config.modes[id]?.role ?? ["main"];
|
|
130
|
+
const ids = Object.keys(config.modes);
|
|
131
|
+
if (ids.length === 0) return "session-mode: `modes` must declare at least one mode";
|
|
132
|
+
if (!ids.includes(config.default)) return `session-mode: \`default\` names ${JSON.stringify(config.default)}, which is not in modes (${ids.join(", ")})`;
|
|
133
|
+
const roleless = ids.filter((id) => roles(id).length === 0);
|
|
134
|
+
if (roleless.length > 0) return `session-mode: mode(s) ${roleless.join(", ")} declare no \`role\`; declare main and/or subagent instead of leaving it empty`;
|
|
135
|
+
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`;
|
|
136
|
+
const unknownPolicies = Object.entries(config.modes).flatMap(([id, mode]) => [...mode.allowPolicies ?? [], ...mode.denyPolicies ?? []].filter((policy) => !POLICY_NAME_SET.has(policy)).map((policy) => `${id}: ${policy}`));
|
|
137
|
+
if (unknownPolicies.length > 0) return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${require_shared.POLICY_NAMES.join(", ")}`;
|
|
138
|
+
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`";
|
|
139
|
+
const retiredPresets = Object.entries(config.modes).filter(([, mode]) => mode.preset !== void 0).map(([id]) => id);
|
|
140
|
+
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)`;
|
|
141
|
+
const partial = Object.entries(config.modes).filter(([, mode]) => {
|
|
142
|
+
const model = mode.defaultModel;
|
|
143
|
+
if (model === void 0) return false;
|
|
144
|
+
return model.provider === void 0 || model.model === void 0 || model.provider.length === 0 || model.model.length === 0;
|
|
145
|
+
}).map(([id]) => id);
|
|
146
|
+
if (partial.length > 0) return `session-mode: mode(s) ${partial.join(", ")} declare \`defaultModel\` without both \`provider\` and \`model\``;
|
|
147
|
+
}
|
|
148
|
+
//#endregion
|
|
149
|
+
//#region src/persona.ts
|
|
150
|
+
const PREFIX_ORDER = "DEPLOYMENT_PERSONA_PREFIX";
|
|
151
|
+
const SUFFIX_ORDER = "DEPLOYMENT_PERSONA_SUFFIX";
|
|
152
|
+
function installPersona(agent, persona) {
|
|
153
|
+
const prompt = agent.ctx.systemPrompt;
|
|
154
|
+
const disposers = [prompt.section({
|
|
155
|
+
name: _deepseek_ai_dsh_system_prompt.PERSONA_PREFIX_SECTION,
|
|
156
|
+
order: prompt.getSectionOrder(PREFIX_ORDER),
|
|
157
|
+
text: persona?.prefix ?? ""
|
|
158
|
+
}), prompt.section({
|
|
159
|
+
name: _deepseek_ai_dsh_system_prompt.PERSONA_SUFFIX_SECTION,
|
|
160
|
+
order: prompt.getSectionOrder(SUFFIX_ORDER),
|
|
161
|
+
text: persona?.suffix ?? ""
|
|
162
|
+
})];
|
|
163
|
+
return () => {
|
|
164
|
+
for (const dispose of disposers) dispose();
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/scope.ts
|
|
169
|
+
const OUT_OF_SCOPE = (toolName, modeName) => `${toolName} 不在「${modeName}」的工具白名单里,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
170
|
+
const DENYLISTED = (toolName, modeName) => `${toolName} 被「${modeName}」的工具黑名单排除,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
171
|
+
function refusalOf(toolName, modeName, denial) {
|
|
172
|
+
return denial === "denylisted" ? DENYLISTED(toolName, modeName) : OUT_OF_SCOPE(toolName, modeName);
|
|
173
|
+
}
|
|
174
|
+
const SKILL_OUT_OF_SCOPE = (skillName, modeName) => `${skillName} 不在「${modeName}」的技能白名单里,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
|
|
175
|
+
const SKILL_DENYLISTED = (skillName, modeName) => `${skillName} 被「${modeName}」的技能黑名单排除,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
|
|
176
|
+
function skillRefusalOf(skillName, modeName, denial) {
|
|
177
|
+
return denial === "denylisted" ? SKILL_DENYLISTED(skillName, modeName) : SKILL_OUT_OF_SCOPE(skillName, modeName);
|
|
178
|
+
}
|
|
179
|
+
const TOOL_SECTION_PREFIX = "tool:";
|
|
180
|
+
const SKILL_TOOL_NAME = "skill";
|
|
181
|
+
function gateOfNames(allow, deny) {
|
|
182
|
+
const denied = new Set(deny);
|
|
183
|
+
const allowed = allow.length === 0 ? void 0 : new Set(allow);
|
|
184
|
+
if (allowed === void 0 && denied.size === 0) return void 0;
|
|
185
|
+
return {
|
|
186
|
+
allow: allowed,
|
|
187
|
+
deny: denied
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function denialOf(gate, name) {
|
|
191
|
+
if (gate.deny.has(name)) return "denylisted";
|
|
192
|
+
if (gate.allow !== void 0 && !gate.allow.has(name)) return "not-allowlisted";
|
|
193
|
+
}
|
|
194
|
+
function kindOf(message) {
|
|
195
|
+
const kind = message.source.kind;
|
|
196
|
+
return typeof kind === "string" ? kind : void 0;
|
|
197
|
+
}
|
|
198
|
+
const CATALOG_LINE = /^- `([^`]+)`: /u;
|
|
199
|
+
function narrowCatalog(message, gate, warn) {
|
|
200
|
+
const source = message.source;
|
|
201
|
+
if (source.kind !== "skill-catalog") return void 0;
|
|
202
|
+
const entries = source.entries;
|
|
203
|
+
const denied = new Set(entries.map((entry) => entry.name).filter((name) => denialOf(gate, name) !== void 0));
|
|
204
|
+
if (denied.size === 0) return void 0;
|
|
205
|
+
const [block, ...rest] = message.content;
|
|
206
|
+
if (block?.type !== "text" || rest.length > 0) {
|
|
207
|
+
warn(`skill catalog message is not one text block; denied skills stay listed: ${[...denied].join(", ")}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const removed = /* @__PURE__ */ new Set();
|
|
211
|
+
const lines = block.text.split("\n").filter((line) => {
|
|
212
|
+
const match = CATALOG_LINE.exec(line);
|
|
213
|
+
if (match === null) return true;
|
|
214
|
+
const name = match[1] ?? "";
|
|
215
|
+
if (!denied.has(name)) return true;
|
|
216
|
+
removed.add(name);
|
|
217
|
+
return false;
|
|
218
|
+
});
|
|
219
|
+
const missing = [...denied].filter((name) => !removed.has(name));
|
|
220
|
+
if (missing.length > 0) {
|
|
221
|
+
warn(`skill catalog lines not recognized; denied skills stay listed: ${missing.join(", ")}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
...message,
|
|
226
|
+
content: [{
|
|
227
|
+
...block,
|
|
228
|
+
text: lines.join("\n")
|
|
229
|
+
}]
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function skillNameOf(arguments_) {
|
|
233
|
+
if (typeof arguments_ !== "object" || arguments_ === null) return void 0;
|
|
234
|
+
const name = arguments_.name;
|
|
235
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
236
|
+
}
|
|
237
|
+
var SessionScope = class {
|
|
238
|
+
applied = /* @__PURE__ */ new WeakMap();
|
|
239
|
+
ctx;
|
|
240
|
+
constructor(ctx) {
|
|
241
|
+
this.ctx = ctx;
|
|
242
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
243
|
+
const agent = context.agent;
|
|
244
|
+
const applied = agent === void 0 ? void 0 : this.applied.get(agent);
|
|
245
|
+
if (applied === void 0 || applied.gate === void 0) return next();
|
|
246
|
+
const gate = applied.gate;
|
|
247
|
+
const result = await next();
|
|
248
|
+
return {
|
|
249
|
+
...result,
|
|
250
|
+
sections: result.sections.filter((section) => section.name.startsWith(TOOL_SECTION_PREFIX) ? denialOf(gate, section.name.slice(5)) === void 0 : true),
|
|
251
|
+
tools: result.tools.filter((tool) => denialOf(gate, tool.name) === void 0)
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
apply(agent, definition) {
|
|
256
|
+
const previous = this.applied.get(agent);
|
|
257
|
+
if (previous !== void 0) previous.dispose();
|
|
258
|
+
const gate = gateOfNames(definition.allowTools, definition.denyTools);
|
|
259
|
+
const skillGate = gateOfNames(definition.allowSkills, definition.denySkills);
|
|
260
|
+
const dispose = agent.ctx.effect(() => {
|
|
261
|
+
const stoppers = [];
|
|
262
|
+
stoppers.push(agent.ctx.on("agent/pre-step", async (_payload, next) => {
|
|
263
|
+
const decision = await next();
|
|
264
|
+
if (decision.kind !== "enter") return decision;
|
|
265
|
+
const kept = this.suppress(decision.messages, definition, skillGate);
|
|
266
|
+
return kept === void 0 ? decision : {
|
|
267
|
+
...decision,
|
|
268
|
+
messages: kept
|
|
269
|
+
};
|
|
270
|
+
}, { prepend: true }));
|
|
271
|
+
if (definition.runtimeContext === false) stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
|
|
272
|
+
const fiber = agent.ctx.inject(["tools"], (scope) => {
|
|
273
|
+
if (gate === void 0 && skillGate === void 0) return;
|
|
274
|
+
scope.tools.guard((exec) => {
|
|
275
|
+
const denial = gate === void 0 ? void 0 : denialOf(gate, exec.name);
|
|
276
|
+
if (denial !== void 0) return refusalOf(exec.name, definition.name, denial);
|
|
277
|
+
if (skillGate === void 0 || exec.name !== SKILL_TOOL_NAME) return void 0;
|
|
278
|
+
const skillName = skillNameOf(exec.arguments);
|
|
279
|
+
if (skillName === void 0) return void 0;
|
|
280
|
+
const skillDenial = denialOf(skillGate, skillName);
|
|
281
|
+
return skillDenial === void 0 ? void 0 : skillRefusalOf(skillName, definition.name, skillDenial);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
return [...stoppers, () => fiber.dispose()];
|
|
285
|
+
}, "session-mode: per-session gate");
|
|
286
|
+
this.channel()?.setInstructions(agent, definition.instructions);
|
|
287
|
+
this.applied.set(agent, {
|
|
288
|
+
gate,
|
|
289
|
+
dispose
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
suppress(messages, definition, skillGate) {
|
|
293
|
+
let changed = false;
|
|
294
|
+
const kept = [];
|
|
295
|
+
for (const message of messages) {
|
|
296
|
+
const kind = kindOf(message);
|
|
297
|
+
if (kind === "agent-instructions") {
|
|
298
|
+
if (definition.instructions) kept.push(message);
|
|
299
|
+
else changed = true;
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (kind === "skill-catalog") {
|
|
303
|
+
if (!definition.skills) {
|
|
304
|
+
changed = true;
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
const narrowed = skillGate === void 0 ? void 0 : narrowCatalog(message, skillGate, (line) => this.ctx.logger.warn(`session-mode: ${line}`));
|
|
308
|
+
if (narrowed === void 0) kept.push(message);
|
|
309
|
+
else {
|
|
310
|
+
changed = true;
|
|
311
|
+
kept.push(narrowed);
|
|
312
|
+
}
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
if (kind === "time-context") {
|
|
316
|
+
if (definition.runtimeContext) kept.push(message);
|
|
317
|
+
else changed = true;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
kept.push(message);
|
|
321
|
+
}
|
|
322
|
+
return changed ? kept : void 0;
|
|
323
|
+
}
|
|
324
|
+
channel() {
|
|
325
|
+
try {
|
|
326
|
+
return this.ctx.get("contextAssembler");
|
|
327
|
+
} catch {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
//#endregion
|
|
333
|
+
//#region src/index.ts
|
|
334
|
+
const name = "session-mode";
|
|
335
|
+
const inject = [
|
|
336
|
+
"agents",
|
|
337
|
+
"sessions",
|
|
338
|
+
"sessionProjections",
|
|
339
|
+
"systemPrompt"
|
|
340
|
+
];
|
|
341
|
+
const sessionModeSchema = zod.z.union([zod.z.string(), zod.z.null()]);
|
|
342
|
+
const sessionModeProjection = {
|
|
343
|
+
key: "sessionMode",
|
|
344
|
+
stateSchema: sessionModeSchema,
|
|
345
|
+
init: () => null,
|
|
346
|
+
apply: (state, event) => event.type === "session-mode/selected" ? event.data.sessionMode : state,
|
|
347
|
+
wire: {
|
|
348
|
+
viewSchema: sessionModeSchema,
|
|
349
|
+
view: (state) => state
|
|
350
|
+
},
|
|
351
|
+
stateVersion: 1
|
|
352
|
+
};
|
|
353
|
+
const sessionModeEditableSchema = zod.z.boolean();
|
|
354
|
+
const sessionModeEditableProjection = {
|
|
355
|
+
key: "sessionModeEditable",
|
|
356
|
+
stateSchema: sessionModeEditableSchema,
|
|
357
|
+
init: () => true,
|
|
358
|
+
apply: (state, event) => state && event.type !== "turn/start",
|
|
359
|
+
wire: {
|
|
360
|
+
viewSchema: sessionModeEditableSchema,
|
|
361
|
+
view: (state) => state
|
|
362
|
+
},
|
|
363
|
+
stateVersion: 1
|
|
364
|
+
};
|
|
365
|
+
var SessionModes = class extends _deepseek_ai_cordis.Service {
|
|
366
|
+
installs = /* @__PURE__ */ new WeakMap();
|
|
367
|
+
scope;
|
|
368
|
+
config;
|
|
369
|
+
constructor(ctx, config) {
|
|
370
|
+
super(ctx, "sessionModes");
|
|
371
|
+
this.config = {
|
|
372
|
+
default: config.default.get(),
|
|
373
|
+
modes: structuredClone(config.modes.get())
|
|
374
|
+
};
|
|
375
|
+
const problem = configProblem({
|
|
376
|
+
...this.config,
|
|
377
|
+
models: config.models
|
|
378
|
+
});
|
|
379
|
+
if (problem !== void 0) throw new Error(problem);
|
|
380
|
+
this.scope = new SessionScope(ctx);
|
|
381
|
+
ctx.sessionProjections.register(sessionModeProjection);
|
|
382
|
+
ctx.sessionProjections.register(sessionModeEditableProjection);
|
|
383
|
+
ctx.on("fs/write-intent", (_target, actor, next) => this.decidePolicy("fs/write-intent", actor, next), { prepend: true });
|
|
384
|
+
ctx.on("fs/edit-intent", (_target, actor, next) => this.decidePolicy("fs/edit-intent", actor, next), { prepend: true });
|
|
385
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
386
|
+
this.installFor(agent);
|
|
387
|
+
});
|
|
388
|
+
ctx.on("agent-preset/selected", (sessionId, preset) => {
|
|
389
|
+
const mapped = this.modeForPreset(preset);
|
|
390
|
+
const agent = ctx.agents.get(sessionId);
|
|
391
|
+
if (mapped === void 0 || agent === void 0) return;
|
|
392
|
+
if (this.ctx.sessionProjections.stateOf(agent.session, "sessionMode") !== mapped) agent.session.append("session-mode/selected", { sessionMode: mapped });
|
|
393
|
+
this.installFor(agent, mapped);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
get defaultId() {
|
|
397
|
+
return this.config.default;
|
|
398
|
+
}
|
|
399
|
+
list() {
|
|
400
|
+
return this.idsFor("main").map((id) => {
|
|
401
|
+
const mode = this.definition(id);
|
|
402
|
+
return {
|
|
403
|
+
id,
|
|
404
|
+
name: mode.name,
|
|
405
|
+
...mode.description === "" ? {} : { description: mode.description }
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
idsFor(role) {
|
|
410
|
+
return Object.entries(this.config.modes).filter(([, mode]) => mode.role.includes(role)).map(([id]) => id);
|
|
411
|
+
}
|
|
412
|
+
modesFor(role) {
|
|
413
|
+
return this.idsFor(role).map((id) => ({
|
|
414
|
+
id,
|
|
415
|
+
mode: this.definition(id)
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
roster() {
|
|
419
|
+
return {
|
|
420
|
+
default: this.defaultId,
|
|
421
|
+
modes: this.list()
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
definition(id) {
|
|
425
|
+
const wanted = id ?? this.defaultId;
|
|
426
|
+
const mode = this.config.modes[wanted];
|
|
427
|
+
if (mode === void 0) throw new Error(`未知的模式 ${JSON.stringify(wanted)};可用的是 ${Object.keys(this.config.modes).join(", ")}`);
|
|
428
|
+
return mode;
|
|
429
|
+
}
|
|
430
|
+
modeOf(session) {
|
|
431
|
+
return this.ctx.sessionProjections.stateOf(session, "sessionMode") ?? this.defaultId;
|
|
432
|
+
}
|
|
433
|
+
modeOfSession(session) {
|
|
434
|
+
return this.definition(this.modeOf(session));
|
|
435
|
+
}
|
|
436
|
+
policyInForce(policy, actor) {
|
|
437
|
+
const agent = actor?.agent;
|
|
438
|
+
if (agent === void 0) return true;
|
|
439
|
+
const mode = this.config.modes[this.modeOf(agent.session)];
|
|
440
|
+
if (mode === void 0) return true;
|
|
441
|
+
if (mode.denyPolicies.includes(policy)) return false;
|
|
442
|
+
return mode.allowPolicies.length === 0 || mode.allowPolicies.includes(policy);
|
|
443
|
+
}
|
|
444
|
+
async decidePolicy(policy, actor, next) {
|
|
445
|
+
if (this.policyInForce(policy, actor)) return await next();
|
|
446
|
+
try {
|
|
447
|
+
await next();
|
|
448
|
+
} catch {}
|
|
449
|
+
}
|
|
450
|
+
async select(sessionId, mode) {
|
|
451
|
+
const definition = this.definition(mode);
|
|
452
|
+
if (!definition.role.includes("main")) throw new Error(`模式 ${JSON.stringify(mode)} 不是用户可选的(它的 role 里没有 main)。`);
|
|
453
|
+
const session = this.ctx.sessions.get(sessionId);
|
|
454
|
+
if (session === void 0) throw new Error(`未知的会话 ${sessionId}`);
|
|
455
|
+
if (this.ctx.sessionProjections.stateOf(session, "sessionModeEditable") === false) throw new Error("这个会话已经开始,模式不能再改;要换模式请新开一个会话。");
|
|
456
|
+
const agent = this.ctx.agents.get(sessionId);
|
|
457
|
+
const registry = this.presetRegistry();
|
|
458
|
+
if (agent !== void 0 && registry !== void 0) {
|
|
459
|
+
const target = this.presetTarget(definition.presetsOnly, this.presetOfAgent(registry, agent));
|
|
460
|
+
if (target !== void 0) await registry.select(agent, target);
|
|
461
|
+
}
|
|
462
|
+
if (this.ctx.sessionProjections.stateOf(session, "sessionMode") !== mode) session.append("session-mode/selected", { sessionMode: mode });
|
|
463
|
+
if (agent !== void 0) this.installFor(agent);
|
|
464
|
+
return mode;
|
|
465
|
+
}
|
|
466
|
+
presetTarget(allowed, current) {
|
|
467
|
+
if (allowed.length === 0) return void 0;
|
|
468
|
+
if (current !== void 0 && allowed.includes(current)) return void 0;
|
|
469
|
+
const target = allowed[0];
|
|
470
|
+
return target === current ? void 0 : target;
|
|
471
|
+
}
|
|
472
|
+
presetOfAgent(registry, agent) {
|
|
473
|
+
const read = registry.composedPreset;
|
|
474
|
+
if (typeof read !== "function") return void 0;
|
|
475
|
+
try {
|
|
476
|
+
return read.call(registry, agent.ctx);
|
|
477
|
+
} catch {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
presetRegistry() {
|
|
482
|
+
try {
|
|
483
|
+
return this.ctx.get("agentPresets");
|
|
484
|
+
} catch {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
applyTo(agent, mode, options = {}) {
|
|
489
|
+
this.definition(mode);
|
|
490
|
+
if (options.record !== false) agent.session.append("session-mode/selected", { sessionMode: mode });
|
|
491
|
+
this.installFor(agent, mode);
|
|
492
|
+
}
|
|
493
|
+
resolveModeId(agent) {
|
|
494
|
+
const selected = this.ctx.sessionProjections.stateOf(agent.session, "sessionMode");
|
|
495
|
+
if (typeof selected === "string") return selected;
|
|
496
|
+
const mapped = this.modeForPreset(this.presetOf(agent.session));
|
|
497
|
+
if (mapped !== void 0) {
|
|
498
|
+
agent.session.append("session-mode/selected", { sessionMode: mapped });
|
|
499
|
+
return mapped;
|
|
500
|
+
}
|
|
501
|
+
const inherited = this.inheritedModeId(agent);
|
|
502
|
+
if (inherited === void 0) return this.defaultId;
|
|
503
|
+
agent.session.append("session-mode/selected", { sessionMode: inherited });
|
|
504
|
+
return inherited;
|
|
505
|
+
}
|
|
506
|
+
presetOf(session) {
|
|
507
|
+
try {
|
|
508
|
+
const state = this.ctx.sessionProjections.stateOf(session, "agentPreset");
|
|
509
|
+
return typeof state === "string" ? state : void 0;
|
|
510
|
+
} catch {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
modeForPreset(preset) {
|
|
515
|
+
if (preset === void 0) return void 0;
|
|
516
|
+
const owners = Object.keys(this.config.modes).filter((id) => this.config.modes[id]?.presetsOnly.includes(preset));
|
|
517
|
+
return owners.length === 1 ? owners[0] : void 0;
|
|
518
|
+
}
|
|
519
|
+
inheritedModeId(agent) {
|
|
520
|
+
const parentId = agent.session.header.parentSession;
|
|
521
|
+
if (parentId === void 0) return void 0;
|
|
522
|
+
const parent = this.ctx.agents.get(parentId);
|
|
523
|
+
return parent === void 0 ? void 0 : this.modeOf(parent.session);
|
|
524
|
+
}
|
|
525
|
+
installFor(agent, modeId = this.resolveModeId(agent)) {
|
|
526
|
+
const installed = this.installs.get(agent);
|
|
527
|
+
if (installed?.mode === modeId) return;
|
|
528
|
+
installed?.dispose();
|
|
529
|
+
const mode = this.definition(modeId);
|
|
530
|
+
const disposers = [installPersona(agent, mode.persona), this.installDefaultModel(agent, modeId)];
|
|
531
|
+
this.installs.set(agent, {
|
|
532
|
+
mode: modeId,
|
|
533
|
+
dispose: () => {
|
|
534
|
+
for (const dispose of disposers) dispose();
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
this.scope.apply(agent, {
|
|
538
|
+
name: mode.name,
|
|
539
|
+
allowTools: mode.allowTools,
|
|
540
|
+
denyTools: mode.denyTools,
|
|
541
|
+
allowSkills: mode.allowSkills,
|
|
542
|
+
denySkills: mode.denySkills,
|
|
543
|
+
instructions: mode.instructions,
|
|
544
|
+
skills: mode.skills ?? derivedSkills(mode),
|
|
545
|
+
runtimeContext: mode.runtimeContext
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
installDefaultModel(agent, modeId) {
|
|
549
|
+
return agent.ctx.on("agent/request", async (_payload, next) => {
|
|
550
|
+
const resolved = await next();
|
|
551
|
+
const model = this.config.modes[modeId]?.defaultModel;
|
|
552
|
+
if (model === void 0) return resolved;
|
|
553
|
+
const pending = this.ctx.sessionProjections.stateOf(agent.session, "modelSelection")?.pending;
|
|
554
|
+
if (pending !== void 0 && pending !== null) return resolved;
|
|
555
|
+
if (agent.session.requestHeader() !== void 0) return resolved;
|
|
556
|
+
return {
|
|
557
|
+
...resolved,
|
|
558
|
+
provider: model.provider,
|
|
559
|
+
model: model.model,
|
|
560
|
+
...model.reasoningEffort === void 0 ? {} : { reasoningEffort: (0, _deepseek_ai_dsh_llm.ReasoningEffortId)(model.reasoningEffort) }
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
function apply(ctx, config) {
|
|
566
|
+
registerHttpRoutes(ctx, new SessionModes(ctx, config));
|
|
567
|
+
ctx.plugin(require_catalog.SessionModeCatalog);
|
|
568
|
+
}
|
|
569
|
+
function respondJson(response, status, body) {
|
|
570
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
571
|
+
response.end(JSON.stringify(body));
|
|
572
|
+
}
|
|
573
|
+
function requestJson(request) {
|
|
574
|
+
return new Promise((resolve, reject) => {
|
|
575
|
+
const chunks = [];
|
|
576
|
+
request.on("data", (chunk) => {
|
|
577
|
+
chunks.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
|
|
578
|
+
});
|
|
579
|
+
request.on("end", () => {
|
|
580
|
+
try {
|
|
581
|
+
resolve(chunks.length === 0 ? void 0 : JSON.parse(chunks.join("")));
|
|
582
|
+
} catch {
|
|
583
|
+
reject(/* @__PURE__ */ new TypeError("请求体不是合法 JSON。"));
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
request.on("error", reject);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function requiredString(value, field) {
|
|
590
|
+
if (typeof value !== "string" || value.length === 0) throw new TypeError(`${field} 必须是非空字符串。`);
|
|
591
|
+
return value;
|
|
592
|
+
}
|
|
593
|
+
async function handleRoute(modes, request, response) {
|
|
594
|
+
try {
|
|
595
|
+
if (request.method === "GET") {
|
|
596
|
+
respondJson(response, 200, modes.roster());
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (request.method === "POST") {
|
|
600
|
+
const body = await requestJson(request);
|
|
601
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) throw new TypeError("请求体必须是 JSON 对象。");
|
|
602
|
+
const record = body;
|
|
603
|
+
const sessionId = requiredString(record["sessionId"], "sessionId");
|
|
604
|
+
const mode = requiredString(record["mode"], "mode");
|
|
605
|
+
respondJson(response, 200, { mode: await modes.select(sessionId, mode) });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
response.writeHead(405);
|
|
609
|
+
response.end();
|
|
610
|
+
} catch (error) {
|
|
611
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
612
|
+
respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function registerHttpRoutes(ctx, modes) {
|
|
616
|
+
ctx.inject(["webServer"], (scope) => {
|
|
617
|
+
const webServer = scope.get("webServer");
|
|
618
|
+
if (webServer === void 0) return;
|
|
619
|
+
scope.effect(() => webServer.register({
|
|
620
|
+
kind: "exact",
|
|
621
|
+
path: require_shared.SESSION_MODE_PATH,
|
|
622
|
+
handler: (request, response) => handleRoute(modes, request, response)
|
|
623
|
+
}), "session-mode: HTTP route");
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
//#endregion
|
|
627
|
+
exports.Config = Config;
|
|
628
|
+
exports.POLICY_NAMES = require_shared.POLICY_NAMES;
|
|
629
|
+
exports.SESSION_MODE_PATH = require_shared.SESSION_MODE_PATH;
|
|
630
|
+
exports.SessionModes = SessionModes;
|
|
631
|
+
exports.apply = apply;
|
|
632
|
+
exports.inject = inject;
|
|
633
|
+
exports.name = name;
|
|
634
|
+
exports.sessionModeEditableProjection = sessionModeEditableProjection;
|
|
635
|
+
exports.sessionModeProjection = sessionModeProjection;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Context, Service, Volatile } from "@deepseek-ai/cordis";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import z$1 from "@deepseek-ai/schemastery";
|
|
4
2
|
import { Agent } from "@deepseek-ai/dsh-agent";
|
|
5
3
|
import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import z$1 from "@deepseek-ai/schemastery";
|
|
6
6
|
//#region src/modes.d.ts
|
|
7
7
|
interface SessionModePersona {
|
|
8
8
|
readonly prefix: string;
|