@morlay/dsh-session-mode 0.0.1 → 0.0.3
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 +51 -110
- package/dist/client.cjs +16 -70
- package/dist/client.d.cts +0 -15
- package/dist/client.d.mts +0 -15
- package/dist/index.d.cts +10 -171
- package/dist/index.d.mts +10 -171
- package/dist/index.mjs +149 -162
- package/dist/rows.d.cts +1 -27
- package/dist/rows.d.mts +1 -27
- package/dist/rows.mjs +15 -452
- package/dist/shared-DX2Giyh3.mjs +5 -0
- package/locale/en.json +2 -2
- package/locale/zh.json +1 -1
- package/package.json +36 -34
- package/src/client/SessionModeSeat.tsx +7 -24
- package/src/client/api.ts +4 -14
- package/src/client/index.ts +34 -31
- package/src/client/locales.ts +2 -6
- package/src/client/use-roster.ts +3 -5
- package/src/index.ts +124 -147
- package/src/mode-sources.ts +42 -46
- package/src/modes.ts +136 -115
- package/src/persona.ts +3 -16
- package/src/rows.ts +26 -14
- package/src/scope.ts +197 -0
- package/src/shared.ts +18 -14
- package/dist/shared-ChCbbJa8.mjs +0 -14
package/dist/index.mjs
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as SESSION_MODE_PATH, t as POLICY_NAMES } from "./shared-DX2Giyh3.mjs";
|
|
2
2
|
import { Service } from "@deepseek-ai/cordis";
|
|
3
3
|
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import z$1 from "@deepseek-ai/schemastery";
|
|
6
6
|
import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
|
|
7
7
|
//#region src/modes.ts
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*/
|
|
8
|
+
function derivedSkills(mode) {
|
|
9
|
+
return (mode.allowTools.length === 0 || mode.allowTools.includes("skill")) && !mode.denyTools.includes("skill");
|
|
10
|
+
}
|
|
12
11
|
const localized = (text) => text;
|
|
13
12
|
const personaSchema = z$1.object({
|
|
14
13
|
prefix: z$1.string().default(""),
|
|
15
14
|
suffix: z$1.string().default("")
|
|
16
15
|
});
|
|
17
|
-
/** 角色是个封闭集合:写错的 role 在装配期就拒绝,而不是静默变成"谁都不用"。 */
|
|
18
16
|
const roleSchema = z$1.union([z$1.const("main"), z$1.const("subagent")]);
|
|
19
|
-
/** 默认模型的形状与全局 `agent-default-model` 一致;省略 effort 就跟 provider 默认。 */
|
|
20
17
|
const modelSchema = z$1.object({
|
|
21
18
|
provider: z$1.string().role("select", { source: "llm-providers" }).description(localized({
|
|
22
19
|
zh: "服务商 id(部署里注册的任意路由,不论哪个适配器插件注册的)。",
|
|
@@ -33,8 +30,8 @@ const modelSchema = z$1.object({
|
|
|
33
30
|
});
|
|
34
31
|
const modeSchema = z$1.object({
|
|
35
32
|
preset: z$1.string().default("").description(localized({
|
|
36
|
-
zh: "这个模式挂哪个 agent preset(它的 id
|
|
37
|
-
en: "Which agent preset this mode rides on (its id, shipped or deployment-owned): that preset supplies the row list and several modes may share it
|
|
33
|
+
zh: "这个模式挂哪个 agent preset(它的 id,官方或本部署自建的):行清单由那份 preset 提供,几个模式可以共享同一个。留空就是不绑——选这个模式不换 preset,会话保持当前挂着的那份,模式自己的提示词、工具收口与开关照常生效。",
|
|
34
|
+
en: "Which agent preset this mode rides on (its id, shipped or deployment-owned): that preset supplies the row list, and several modes may share it. Leave it empty to bind none — selecting the mode then keeps whatever preset the session already has, while the mode's persona, tool narrowing, and switches still apply."
|
|
38
35
|
})),
|
|
39
36
|
name: z$1.string().required().description(localized({
|
|
40
37
|
zh: "模式名(事实文案;选择面归官方 roster)。",
|
|
@@ -50,21 +47,33 @@ const modeSchema = z$1.object({
|
|
|
50
47
|
})),
|
|
51
48
|
persona: personaSchema.default({}),
|
|
52
49
|
allowTools: z$1.array(z$1.string()).default([]).description(localized({
|
|
53
|
-
zh: "
|
|
54
|
-
en: "Tools this session may use; everything else leaves the catalog and calls are refused."
|
|
50
|
+
zh: "这个会话能用的工具;其余既不进目录,调用也被拒。**留空就是不设收窄**:用这个会话挂着的 preset 提供的全部工具。",
|
|
51
|
+
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."
|
|
52
|
+
})),
|
|
53
|
+
denyTools: z$1.array(z$1.string()).default([]).description(localized({
|
|
54
|
+
zh: "这个模式不用的工具(黑名单):从 `allowTools` 定下的那份里减掉(`allowTools` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
55
|
+
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."
|
|
56
|
+
})),
|
|
57
|
+
allowPolicies: z$1.array(z$1.string()).default([]).description(localized({
|
|
58
|
+
zh: "上游 policy 规则的生效白名单(`fs/write-intent` / `fs/edit-intent`):留空 = 全部规则照旧生效;有值 = 只有列出的生效,其余的被绕过。",
|
|
59
|
+
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."
|
|
60
|
+
})),
|
|
61
|
+
denyPolicies: z$1.array(z$1.string()).default([]).description(localized({
|
|
62
|
+
zh: "上游 policy 规则的黑名单:列出的规则禁用(它的裁决连拒绝一起丢掉,调用按没有这条规则继续)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
63
|
+
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."
|
|
55
64
|
})),
|
|
56
65
|
instructions: z$1.boolean().default(true).description(localized({
|
|
57
|
-
zh: "是否要 instruction
|
|
58
|
-
en: "Whether instruction-class injections apply (workspace instructions,
|
|
66
|
+
zh: "是否要 instruction 类注入(工作区指令、用法正文):`false` 时丢掉官方 `agent-instructions` 的注入,并关掉通道自己的降级注入。",
|
|
67
|
+
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."
|
|
68
|
+
})),
|
|
69
|
+
skills: z$1.boolean().description(localized({
|
|
70
|
+
zh: "是否要技能目录(官方 `skill-catalog` 的注入)。**不写就按这个模式自己的工具名单推导**:`(allowTools 留空 ? 全部 : allowTools) − denyTools` 里含 `skill` 就要;写 `false` 就丢掉官方 `skill-catalog` 的注入(`skill` 工具的可见性仍归 `allowTools`)。",
|
|
71
|
+
en: "Whether the skill catalog applies (the official `skill-catalog` injection). Unset 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`)."
|
|
59
72
|
})),
|
|
60
73
|
runtimeContext: z$1.boolean().default(true).description(localized({
|
|
61
74
|
zh: "是否要动态快照(文件沙箱策略、审批策略)。",
|
|
62
75
|
en: "Whether the runtime snapshot applies (sandbox and approval policy)."
|
|
63
76
|
})),
|
|
64
|
-
/**
|
|
65
|
-
* 这个模式的默认模型。不标 `volatile`:`modes` 本身就是 volatile,整棵子树都在页面上——再标一层会被
|
|
66
|
-
* schemastery 拒(`validateVolatileSchema` 不许 volatile 套 volatile)。
|
|
67
|
-
*/
|
|
68
77
|
defaultModel: modelSchema.default(null).description(localized({
|
|
69
78
|
zh: "这个模式的默认模型;省略就跟全局默认模型。只在会话还没有模型事实时接管。",
|
|
70
79
|
en: "Default model for this mode; unset follows the global default. Applies only while a session has no model fact yet."
|
|
@@ -76,18 +85,13 @@ const Config = z$1.object({
|
|
|
76
85
|
en: "Mode a session starts in before anyone picks one; must be an id in `modes`."
|
|
77
86
|
})).volatile(),
|
|
78
87
|
modes: z$1.dict(modeSchema).required().description(localized({
|
|
79
|
-
zh: "模式清单:id → 定义(persona /
|
|
80
|
-
en: "Mode roster: id to definition (persona,
|
|
88
|
+
zh: "模式清单:id → 定义(persona / 工具名单 / 生效的 policy 规则 / 角色)。改它对**已运行会话**不自动生效——重挂后新建的会话、或重新应用模式的会话才用新定义。",
|
|
89
|
+
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."
|
|
81
90
|
})).volatile(),
|
|
82
|
-
/**
|
|
83
|
-
* 退役字段:默认模型住在每个模式自己的 `defaultModel` 里。这里留着是为了**报错**(见 `configProblem`),
|
|
84
|
-
* 页面上不出现(`hidden()`)。
|
|
85
|
-
*/
|
|
86
91
|
models: z$1.dict(modelSchema).default({}).hidden()
|
|
87
92
|
});
|
|
88
|
-
|
|
93
|
+
const POLICY_NAME_SET = new Set(POLICY_NAMES);
|
|
89
94
|
function configProblem(config) {
|
|
90
|
-
/** 没写 `role` 等于默认 `["main"]`(与 schema 的默认一致)——字面量与归一化后的形状都能校验。 */
|
|
91
95
|
const roles = (id) => config.modes[id]?.role ?? ["main"];
|
|
92
96
|
const ids = Object.keys(config.modes);
|
|
93
97
|
if (ids.length === 0) return "session-mode: `modes` must declare at least one mode";
|
|
@@ -95,8 +99,8 @@ function configProblem(config) {
|
|
|
95
99
|
const roleless = ids.filter((id) => roles(id).length === 0);
|
|
96
100
|
if (roleless.length > 0) return `session-mode: mode(s) ${roleless.join(", ")} declare no \`role\`; declare main and/or subagent instead of leaving it empty`;
|
|
97
101
|
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`;
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
102
|
+
const unknownPolicies = Object.entries(config.modes).flatMap(([id, mode]) => [...mode.allowPolicies ?? [], ...mode.denyPolicies ?? []].filter((policy) => !POLICY_NAME_SET.has(policy)).map((policy) => `${id}: ${policy}`));
|
|
103
|
+
if (unknownPolicies.length > 0) return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${POLICY_NAMES.join(", ")}`;
|
|
100
104
|
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`";
|
|
101
105
|
const partial = Object.entries(config.modes).filter(([, mode]) => {
|
|
102
106
|
const model = mode.defaultModel;
|
|
@@ -109,12 +113,6 @@ function configProblem(config) {
|
|
|
109
113
|
//#region src/persona.ts
|
|
110
114
|
const PREFIX_ORDER = "DEPLOYMENT_PERSONA_PREFIX";
|
|
111
115
|
const SUFFIX_ORDER = "DEPLOYMENT_PERSONA_SUFFIX";
|
|
112
|
-
/**
|
|
113
|
-
* 把一段 persona 装到该 agent 的 scope 上。
|
|
114
|
-
* @param agent - 目标 agent(用它的 `ctx` 定作用域)。
|
|
115
|
-
* @param persona - 模式的提示词;缺省时两段都注册成空串,等于遮蔽掉部署级那层。
|
|
116
|
-
* @returns 注销这两个 section 的 disposer(模式切换时先调它)。
|
|
117
|
-
*/
|
|
118
116
|
function installPersona(agent, persona) {
|
|
119
117
|
const prompt = agent.ctx.systemPrompt;
|
|
120
118
|
const disposers = [prompt.section({
|
|
@@ -131,39 +129,99 @@ function installPersona(agent, persona) {
|
|
|
131
129
|
};
|
|
132
130
|
}
|
|
133
131
|
//#endregion
|
|
132
|
+
//#region src/scope.ts
|
|
133
|
+
const OUT_OF_SCOPE = (toolName, modeName) => `${toolName} 不在「${modeName}」的工具白名单里,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
134
|
+
const DENYLISTED = (toolName, modeName) => `${toolName} 被「${modeName}」的工具黑名单排除,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
135
|
+
function refusalOf(toolName, modeName, denial) {
|
|
136
|
+
return denial === "denylisted" ? DENYLISTED(toolName, modeName) : OUT_OF_SCOPE(toolName, modeName);
|
|
137
|
+
}
|
|
138
|
+
const TOOL_SECTION_PREFIX = "tool:";
|
|
139
|
+
function gateOf(definition) {
|
|
140
|
+
const deny = new Set(definition.denyTools);
|
|
141
|
+
const allow = definition.allowTools.length === 0 ? void 0 : new Set(definition.allowTools);
|
|
142
|
+
if (allow === void 0 && deny.size === 0) return void 0;
|
|
143
|
+
return {
|
|
144
|
+
allow,
|
|
145
|
+
deny
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function denialOf(gate, toolName) {
|
|
149
|
+
if (gate.deny.has(toolName)) return "denylisted";
|
|
150
|
+
if (gate.allow !== void 0 && !gate.allow.has(toolName)) return "not-allowlisted";
|
|
151
|
+
}
|
|
152
|
+
function kindOf(message) {
|
|
153
|
+
const kind = message.source.kind;
|
|
154
|
+
return typeof kind === "string" ? kind : void 0;
|
|
155
|
+
}
|
|
156
|
+
var SessionScope = class {
|
|
157
|
+
applied = /* @__PURE__ */ new WeakMap();
|
|
158
|
+
ctx;
|
|
159
|
+
constructor(ctx) {
|
|
160
|
+
this.ctx = ctx;
|
|
161
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
162
|
+
const agent = context.agent;
|
|
163
|
+
const applied = agent === void 0 ? void 0 : this.applied.get(agent);
|
|
164
|
+
if (applied === void 0 || applied.gate === void 0) return next();
|
|
165
|
+
const gate = applied.gate;
|
|
166
|
+
const result = await next();
|
|
167
|
+
return {
|
|
168
|
+
...result,
|
|
169
|
+
sections: result.sections.filter((section) => section.name.startsWith(TOOL_SECTION_PREFIX) ? denialOf(gate, section.name.slice(5)) === void 0 : true),
|
|
170
|
+
tools: result.tools.filter((tool) => denialOf(gate, tool.name) === void 0)
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
ctx.on("agent/pre-step", async (payload, next) => {
|
|
174
|
+
const decision = await next();
|
|
175
|
+
if (decision.kind !== "enter") return decision;
|
|
176
|
+
const applied = this.applied.get(payload.agent);
|
|
177
|
+
if (applied === void 0) return decision;
|
|
178
|
+
const kept = decision.messages.filter((message) => {
|
|
179
|
+
const kind = kindOf(message);
|
|
180
|
+
if (kind === "agent-instructions") return applied.instructions;
|
|
181
|
+
if (kind === "skill-catalog") return applied.skills;
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
return kept.length === decision.messages.length ? decision : {
|
|
185
|
+
...decision,
|
|
186
|
+
messages: kept
|
|
187
|
+
};
|
|
188
|
+
}, { prepend: true });
|
|
189
|
+
}
|
|
190
|
+
apply(agent, definition) {
|
|
191
|
+
const previous = this.applied.get(agent);
|
|
192
|
+
if (previous !== void 0) previous.dispose();
|
|
193
|
+
const gate = gateOf(definition);
|
|
194
|
+
const dispose = agent.ctx.effect(() => {
|
|
195
|
+
const stoppers = [];
|
|
196
|
+
if (definition.runtimeContext === false) stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
|
|
197
|
+
const fiber = agent.ctx.inject(["tools"], (scope) => {
|
|
198
|
+
if (gate === void 0) return;
|
|
199
|
+
scope.tools.guard((exec) => {
|
|
200
|
+
const denial = denialOf(gate, exec.name);
|
|
201
|
+
return denial === void 0 ? void 0 : refusalOf(exec.name, definition.name, denial);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
return [...stoppers, () => fiber.dispose()];
|
|
205
|
+
}, "session-mode: per-session gate");
|
|
206
|
+
this.channel()?.setInstructions(agent, definition.instructions);
|
|
207
|
+
this.applied.set(agent, {
|
|
208
|
+
gate,
|
|
209
|
+
instructions: definition.instructions,
|
|
210
|
+
skills: definition.skills,
|
|
211
|
+
dispose
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
channel() {
|
|
215
|
+
try {
|
|
216
|
+
return this.ctx.get("contextAssembler");
|
|
217
|
+
} catch {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
//#endregion
|
|
134
223
|
//#region src/index.ts
|
|
135
|
-
/**
|
|
136
|
-
* 会话模式的 host 半:模式清单(config 里的纯数据)、会话 ↔ 模式的选择、按会话应用 persona 与收口。
|
|
137
|
-
*
|
|
138
|
-
* **模式不是 Cordis 子树**。旧的 `@morlay/dsh-agent-preset` 每个模式都是一行
|
|
139
|
-
* `@deepseek-ai/dsh-agent-preset`,`config.plugins` 里装 persona 与 scope 行,靠 preset scope 的父链
|
|
140
|
-
* 对会话生效——代价是整个官方 registry(声明式行、每 revision 一棵 Loader 子树、`isolate` realm)都在
|
|
141
|
-
* 部署里。这里只留两件事:模式是一份数据,应用落在会话自己的 scope 上:
|
|
142
|
-
*
|
|
143
|
-
* | 事实 | 落在哪 |
|
|
144
|
-
* | ---------------- | ------------------------------------------------------------------------------------------ |
|
|
145
|
-
* | 模式清单与默认值 | 本行的 `config`(装配层可整体改写;`modes.ts` 给形状与校验) |
|
|
146
|
-
* | 各模式的默认模型 | 同一份 config 的顶层 `models`(**volatile**:设置页那张卡片改的就是它) |
|
|
147
|
-
* | 会话当前模式 | session 事件 `session-mode/selected` + 投影 `sessionMode`(log-only,重建读投影) |
|
|
148
|
-
* | 提示词 | `persona`:把模式的 persona 注册到该 agent 的 scope(`persona.ts`) |
|
|
149
|
-
* | 工具与注入开关 | 推给 `ctx.sessionToolScope`(`@morlay/dsh-context-assembler/scope`,行 id `context-assembler-scope`) |
|
|
150
|
-
* | 页面上的选择面 | HTTP 路由 `GET/POST /session-mode`(清单与切换)+ 会话投影(当前值) |
|
|
151
|
-
*
|
|
152
|
-
* 应用时机是 `agent/created`:它早于任何一次提示词装配(装配发生在 turn 里),所以 persona 一定在该会话
|
|
153
|
-
* 第一次装配之前就注册好了。模式在**空白会话**里可以切换,切换时对着已有的 agent 重新应用一遍。
|
|
154
|
-
*
|
|
155
|
-
* 切换只允许在空白窗口(还没开过 turn):会话的历史是在某个模式的工具集与提示词下产生的,换了模式,那段
|
|
156
|
-
* 历史就与实际装配对不上——与上游 `agentPresets.select` 的判据一致,也用同一个投影(`turnBoundary`)。
|
|
157
|
-
*/
|
|
158
|
-
/** Cordis 插件名:与行 id 一致。 */
|
|
159
224
|
const name = "session-mode";
|
|
160
|
-
/**
|
|
161
|
-
* 依赖:投影服务(登记 `sessionMode`、读 `turnBoundary`)、读会话与 agent 的两个注册表,以及提示词注册表
|
|
162
|
-
* (persona 的 section 注册在它上面)。
|
|
163
|
-
*
|
|
164
|
-
* 这些名字必须在 `inject` 里点名:cordis 的**属性访问**(`this.ctx.sessions`)要求本 fiber 声明过那个服务,
|
|
165
|
-
* 未声明会抛 `cannot get property "sessions" without inject`(`ctx.get(name)` 才不需要声明)。
|
|
166
|
-
*/
|
|
167
225
|
const inject = [
|
|
168
226
|
"agents",
|
|
169
227
|
"sessions",
|
|
@@ -171,7 +229,6 @@ const inject = [
|
|
|
171
229
|
"systemPrompt"
|
|
172
230
|
];
|
|
173
231
|
const sessionModeSchema = z.union([z.string(), z.null()]);
|
|
174
|
-
/** 会话模式的投影:初值来自空日志(没选过就是 `null`),只被选择事件推进。 */
|
|
175
232
|
const sessionModeProjection = {
|
|
176
233
|
key: "sessionMode",
|
|
177
234
|
stateSchema: sessionModeSchema,
|
|
@@ -184,13 +241,6 @@ const sessionModeProjection = {
|
|
|
184
241
|
stateVersion: 1
|
|
185
242
|
};
|
|
186
243
|
const sessionModeEditableSchema = z.boolean();
|
|
187
|
-
/**
|
|
188
|
-
* 这个会话能不能换模式的投影:空白会话为 `true`,一旦 `turn/start` 落库就永远 `false`(换模式要的是**整段
|
|
189
|
-
* 历史**的模式一致,所以"开过 turn"之后连正在跑的那个 turn 也算)。
|
|
190
|
-
*
|
|
191
|
-
* **判据只有这一处**:服务端 {@link SessionModes.select} 的拒绝与 client chip 的只读形态都读它——以前
|
|
192
|
-
* client 只能等服务端报错,现在连入口都不给,而两边的结论来自同一份会话事实。
|
|
193
|
-
*/
|
|
194
244
|
const sessionModeEditableProjection = {
|
|
195
245
|
key: "sessionModeEditable",
|
|
196
246
|
stateSchema: sessionModeEditableSchema,
|
|
@@ -202,11 +252,9 @@ const sessionModeEditableProjection = {
|
|
|
202
252
|
},
|
|
203
253
|
stateVersion: 1
|
|
204
254
|
};
|
|
205
|
-
/** 模式清单、默认模式、按会话读取与切换。 */
|
|
206
255
|
var SessionModes = class extends Service {
|
|
207
|
-
/** 每个 agent 已经装上的那一份(persona + 默认模型兜底;模式变了就换一份)。 */
|
|
208
256
|
installs = /* @__PURE__ */ new WeakMap();
|
|
209
|
-
|
|
257
|
+
scope;
|
|
210
258
|
config;
|
|
211
259
|
constructor(ctx, config) {
|
|
212
260
|
super(ctx, "sessionModes");
|
|
@@ -219,8 +267,11 @@ var SessionModes = class extends Service {
|
|
|
219
267
|
models: config.models
|
|
220
268
|
});
|
|
221
269
|
if (problem !== void 0) throw new Error(problem);
|
|
270
|
+
this.scope = new SessionScope(ctx);
|
|
222
271
|
ctx.sessionProjections.register(sessionModeProjection);
|
|
223
272
|
ctx.sessionProjections.register(sessionModeEditableProjection);
|
|
273
|
+
ctx.on("fs/write-intent", (_target, actor, next) => this.decidePolicy("fs/write-intent", actor, next), { prepend: true });
|
|
274
|
+
ctx.on("fs/edit-intent", (_target, actor, next) => this.decidePolicy("fs/edit-intent", actor, next), { prepend: true });
|
|
224
275
|
ctx.on("agent/created", ({ agent }) => {
|
|
225
276
|
this.installFor(agent);
|
|
226
277
|
});
|
|
@@ -232,11 +283,9 @@ var SessionModes = class extends Service {
|
|
|
232
283
|
this.installFor(agent, mapped);
|
|
233
284
|
});
|
|
234
285
|
}
|
|
235
|
-
/** 新会话用它:config 里的 `default`。 */
|
|
236
286
|
get defaultId() {
|
|
237
287
|
return this.config.default;
|
|
238
288
|
}
|
|
239
|
-
/** 选择器要的清单:只列 `main` 角色的模式(id、展示名、说明,顺序即 config 里 `modes` 的插入序)。 */
|
|
240
289
|
list() {
|
|
241
290
|
return this.idsFor("main").map((id) => {
|
|
242
291
|
const mode = this.definition(id);
|
|
@@ -247,60 +296,47 @@ var SessionModes = class extends Service {
|
|
|
247
296
|
};
|
|
248
297
|
});
|
|
249
298
|
}
|
|
250
|
-
/**
|
|
251
|
-
* 声明了某个角色的模式 id(顺序即 config 的插入序):`main` 给用户选择器,`subagent` 给子代理候选。
|
|
252
|
-
* @param role - 目标角色。
|
|
253
|
-
* @returns 该角色下的模式 id。
|
|
254
|
-
*/
|
|
255
299
|
idsFor(role) {
|
|
256
300
|
return Object.entries(this.config.modes).filter(([, mode]) => mode.role.includes(role)).map(([id]) => id);
|
|
257
301
|
}
|
|
258
|
-
/**
|
|
259
|
-
* 同 {@link idsFor},给的是定义——"指定 mode" 那条接缝要拿候选集。
|
|
260
|
-
* @param role - 目标角色。
|
|
261
|
-
* @returns 该角色下的模式与其定义。
|
|
262
|
-
*/
|
|
263
302
|
modesFor(role) {
|
|
264
303
|
return this.idsFor(role).map((id) => ({
|
|
265
304
|
id,
|
|
266
305
|
mode: this.definition(id)
|
|
267
306
|
}));
|
|
268
307
|
}
|
|
269
|
-
/** 页面用的清单 + 默认模式。 */
|
|
270
308
|
roster() {
|
|
271
309
|
return {
|
|
272
310
|
default: this.defaultId,
|
|
273
311
|
modes: this.list()
|
|
274
312
|
};
|
|
275
313
|
}
|
|
276
|
-
/** 按 id 取定义;未知 id 直接抛(切换路径上它就是用户的错)。 */
|
|
277
314
|
definition(id) {
|
|
278
315
|
const wanted = id ?? this.defaultId;
|
|
279
316
|
const mode = this.config.modes[wanted];
|
|
280
317
|
if (mode === void 0) throw new Error(`未知的模式 ${JSON.stringify(wanted)};可用的是 ${Object.keys(this.config.modes).join(", ")}`);
|
|
281
318
|
return mode;
|
|
282
319
|
}
|
|
283
|
-
/** 会话当前模式:投影上有就用它,否则是部署默认。 */
|
|
284
320
|
modeOf(session) {
|
|
285
321
|
return this.ctx.sessionProjections.stateOf(session, "sessionMode") ?? this.defaultId;
|
|
286
322
|
}
|
|
287
|
-
/** 会话当前模式的定义。 */
|
|
288
323
|
modeOfSession(session) {
|
|
289
324
|
return this.definition(this.modeOf(session));
|
|
290
325
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
326
|
+
policyInForce(policy, actor) {
|
|
327
|
+
const agent = actor?.agent;
|
|
328
|
+
if (agent === void 0) return true;
|
|
329
|
+
const mode = this.config.modes[this.modeOf(agent.session)];
|
|
330
|
+
if (mode === void 0) return true;
|
|
331
|
+
if (mode.denyPolicies.includes(policy)) return false;
|
|
332
|
+
return mode.allowPolicies.length === 0 || mode.allowPolicies.includes(policy);
|
|
333
|
+
}
|
|
334
|
+
async decidePolicy(policy, actor, next) {
|
|
335
|
+
if (this.policyInForce(policy, actor)) return await next();
|
|
336
|
+
try {
|
|
337
|
+
await next();
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
304
340
|
async select(sessionId, mode) {
|
|
305
341
|
const definition = this.definition(mode);
|
|
306
342
|
if (!definition.role.includes("main")) throw new Error(`模式 ${JSON.stringify(mode)} 不是用户可选的(它的 role 里没有 main)。`);
|
|
@@ -316,12 +352,6 @@ var SessionModes = class extends Service {
|
|
|
316
352
|
if (agent !== void 0) this.installFor(agent);
|
|
317
353
|
return mode;
|
|
318
354
|
}
|
|
319
|
-
/**
|
|
320
|
-
* 某个 agent 当前挂着的 preset(registry 的 `composedPreset`)。
|
|
321
|
-
*
|
|
322
|
-
* 读不到时返回 `undefined`(按"未知"处理 → 该切就切):`composedPreset` 是 registry 较新的读面,替身与老
|
|
323
|
-
* 版本可能没有它;而"没挂任何 preset"(返回值 `undefined`)与"读不到"在这里是同一个结论。
|
|
324
|
-
*/
|
|
325
355
|
presetOfAgent(registry, agent) {
|
|
326
356
|
const read = registry.composedPreset;
|
|
327
357
|
if (typeof read !== "function") return void 0;
|
|
@@ -331,10 +361,6 @@ var SessionModes = class extends Service {
|
|
|
331
361
|
return;
|
|
332
362
|
}
|
|
333
363
|
}
|
|
334
|
-
/**
|
|
335
|
-
* 官方 preset registry:行清单与它的选择面住在那一行。不在 `inject` 里点名(headless 部署没有它,
|
|
336
|
-
* 点名会让本行永不激活),所以按"可能拿不到"读——`ctx.get` 在当前 ctx 没声明那个服务时会抛。
|
|
337
|
-
*/
|
|
338
364
|
presetRegistry() {
|
|
339
365
|
try {
|
|
340
366
|
return this.ctx.get("agentPresets");
|
|
@@ -342,23 +368,11 @@ var SessionModes = class extends Service {
|
|
|
342
368
|
return;
|
|
343
369
|
}
|
|
344
370
|
}
|
|
345
|
-
/**
|
|
346
|
-
* 把某个活着的 agent 切到某个模式。与 {@link select} 的差别:它不要求空白会话——子代理创建时的继承
|
|
347
|
-
* 走这里,**未来的"指定 mode"入口(模型侧或配置侧)也走这里**(那条接缝还没做)。
|
|
348
|
-
* @param agent - 目标 agent。
|
|
349
|
-
* @param mode - 目标模式 id。
|
|
350
|
-
* @param options.record - 是否把这次切换写进会话日志(缺省写;只想改当前进程时给 `false`)。
|
|
351
|
-
*/
|
|
352
371
|
applyTo(agent, mode, options = {}) {
|
|
353
372
|
this.definition(mode);
|
|
354
373
|
if (options.record !== false) agent.session.append("session-mode/selected", { sessionMode: mode });
|
|
355
374
|
this.installFor(agent, mode);
|
|
356
375
|
}
|
|
357
|
-
/**
|
|
358
|
-
* 该 agent 用哪个模式:会话选过(投影上有)优先,子代理继承父,其余用部署默认。
|
|
359
|
-
*
|
|
360
|
-
* 继承要**写进子会话日志**:它是一条会话事实,冷恢复与 fork 都要靠它重建({@link modeOf} 只读投影)。
|
|
361
|
-
*/
|
|
362
376
|
resolveModeId(agent) {
|
|
363
377
|
const selected = this.ctx.sessionProjections.stateOf(agent.session, "sessionMode");
|
|
364
378
|
if (typeof selected === "string") return selected;
|
|
@@ -372,7 +386,6 @@ var SessionModes = class extends Service {
|
|
|
372
386
|
agent.session.append("session-mode/selected", { sessionMode: inherited });
|
|
373
387
|
return inherited;
|
|
374
388
|
}
|
|
375
|
-
/** 官方 roster 选定的 preset(没选过、或 registry 没装时为 `undefined`)。 */
|
|
376
389
|
presetOf(session) {
|
|
377
390
|
try {
|
|
378
391
|
const state = this.ctx.sessionProjections.stateOf(session, "agentPreset");
|
|
@@ -381,29 +394,17 @@ var SessionModes = class extends Service {
|
|
|
381
394
|
return;
|
|
382
395
|
}
|
|
383
396
|
}
|
|
384
|
-
/**
|
|
385
|
-
* 某个 agent preset 对应的模式 id——**只在映射唯一时**回答(几个模式挂同一份 preset 时返回
|
|
386
|
-
* `undefined`,不反查)。
|
|
387
|
-
*
|
|
388
|
-
* 本部署的两个模式共享同一个 preset(`MODE_PRESET_ID`),差异全在会话级收口,所以 preset → 模式的反查在
|
|
389
|
-
* 这里无意义:模式由**会话事实**决定(`session-mode/selected` 投影 → 子代理继承 → 部署默认),
|
|
390
|
-
* 官方 roster 选了什么 preset 不改变这个会话是哪个模式。这条反查留给"一对一映射"的部署形态。
|
|
391
|
-
* @param preset - preset id(`undefined` 表示没选过、或 registry 没装)。
|
|
392
|
-
* @returns 该 preset 唯一对应的模式 id;没配扩展、或由多个模式共享时 `undefined`。
|
|
393
|
-
*/
|
|
394
397
|
modeForPreset(preset) {
|
|
395
398
|
if (preset === void 0) return void 0;
|
|
396
399
|
const owners = Object.keys(this.config.modes).filter((id) => this.config.modes[id]?.preset === preset);
|
|
397
400
|
return owners.length === 1 ? owners[0] : void 0;
|
|
398
401
|
}
|
|
399
|
-
/** 子代理(有 durable 父会话)继承父当前模式;父不在场、或不是子代理时没有可继承的。 */
|
|
400
402
|
inheritedModeId(agent) {
|
|
401
403
|
const parentId = agent.session.header.parentSession;
|
|
402
404
|
if (parentId === void 0) return void 0;
|
|
403
405
|
const parent = this.ctx.agents.get(parentId);
|
|
404
406
|
return parent === void 0 ? void 0 : this.modeOf(parent.session);
|
|
405
407
|
}
|
|
406
|
-
/** 装或换该 agent 的那一份(幂等:同一模式不重复注册)。 */
|
|
407
408
|
installFor(agent, modeId = this.resolveModeId(agent)) {
|
|
408
409
|
const installed = this.installs.get(agent);
|
|
409
410
|
if (installed?.mode === modeId) return;
|
|
@@ -416,15 +417,15 @@ var SessionModes = class extends Service {
|
|
|
416
417
|
for (const dispose of disposers) dispose();
|
|
417
418
|
}
|
|
418
419
|
});
|
|
419
|
-
this.
|
|
420
|
+
this.scope.apply(agent, {
|
|
421
|
+
name: mode.name,
|
|
422
|
+
allowTools: mode.allowTools,
|
|
423
|
+
denyTools: mode.denyTools,
|
|
424
|
+
instructions: mode.instructions,
|
|
425
|
+
skills: mode.skills ?? derivedSkills(mode),
|
|
426
|
+
runtimeContext: mode.runtimeContext
|
|
427
|
+
});
|
|
420
428
|
}
|
|
421
|
-
/**
|
|
422
|
-
* 模式的默认模型(`modes.<模式 id>.defaultModel`)兜底:只在会话**尚无任何模型事实**(没选过模型、也
|
|
423
|
-
* 还没跑过请求)时接管这一请求的路由;一旦用户选过(投影 `pending`)或会话已经落过 header,就不再插手。
|
|
424
|
-
*
|
|
425
|
-
* 它是**配置事实**,不写会话事件——重启后仍由 config 决定;设置页里那条会话级选择才是会话事实。读的是构造
|
|
426
|
-
* 时那份模式清单快照:设置页保存会让这一行重挂(`reconcileProfilePatches`),新定义随重挂生效。
|
|
427
|
-
*/
|
|
428
429
|
installDefaultModel(agent, modeId) {
|
|
429
430
|
return agent.ctx.on("agent/request", async (_payload, next) => {
|
|
430
431
|
const resolved = await next();
|
|
@@ -441,14 +442,6 @@ var SessionModes = class extends Service {
|
|
|
441
442
|
};
|
|
442
443
|
});
|
|
443
444
|
}
|
|
444
|
-
/** 收口服务由 `@morlay/dsh-context-assembler/scope` 那一行发布;没装它就只有 persona。 */
|
|
445
|
-
toolScope() {
|
|
446
|
-
try {
|
|
447
|
-
return this.ctx.get("sessionToolScope");
|
|
448
|
-
} catch {
|
|
449
|
-
return;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
445
|
};
|
|
453
446
|
function apply(ctx, config) {
|
|
454
447
|
registerHttpRoutes(ctx, new SessionModes(ctx, config));
|
|
@@ -499,12 +492,6 @@ async function handleRoute(modes, request, response) {
|
|
|
499
492
|
respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
|
|
500
493
|
}
|
|
501
494
|
}
|
|
502
|
-
/**
|
|
503
|
-
* 把清单与切换挂到同一张宿主路由表上。
|
|
504
|
-
*
|
|
505
|
-
* `webServer` **必须等**:它可能比本行晚激活,而一次性 `ctx.get` 取到 `undefined` 之后不会再试一次——
|
|
506
|
-
* 路由没注册的后果是请求落到静态资源 fallback,非 GET/HEAD 一律 405。
|
|
507
|
-
*/
|
|
508
495
|
function registerHttpRoutes(ctx, modes) {
|
|
509
496
|
ctx.inject(["webServer"], (scope) => {
|
|
510
497
|
const webServer = scope.get("webServer");
|
|
@@ -517,4 +504,4 @@ function registerHttpRoutes(ctx, modes) {
|
|
|
517
504
|
});
|
|
518
505
|
}
|
|
519
506
|
//#endregion
|
|
520
|
-
export { Config, SESSION_MODE_PATH, SessionModes, apply, inject, name, sessionModeEditableProjection, sessionModeProjection };
|
|
507
|
+
export { Config, POLICY_NAMES, SESSION_MODE_PATH, SessionModes, apply, inject, name, sessionModeEditableProjection, sessionModeProjection };
|
package/dist/rows.d.cts
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
//#region src/mode-sources.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* 模式定义的真源:`session-mode` 行的 `config` 由 `./rows.ts` 渲染(装配入口在 `packages/bundles/session-mode-profile`)。
|
|
4
|
-
*
|
|
5
|
-
* "自定义"就落在这份数据上——装配层(profile 的用户 patch 层)可以整体改写 `config.modes`,也可以只给
|
|
6
|
-
* 某个模式换提示词或工具白名单,不需要任何插件行。
|
|
7
|
-
*
|
|
8
|
-
* `allowTools` 从 [`@morlay/dsh-agent-toolkit/rows`](../../../dsh-agent-toolkit/src/rows.ts) 的
|
|
9
|
-
* `TOOLKIT_TOOL_NAMES` 派生(工具名与汉化同源):**行清单由本部署自己注册的那份 preset 提供**
|
|
10
|
-
* ({@link MODE_PRESET_ID},两个模式共享它),白名单里那份 preset 没有的工具自动跳过。想改某一个模式的名单,
|
|
11
|
-
* 直接在这条源数据里加/减。
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* 本部署自己注册的那份 agent preset 的 id:两个模式共享它(行清单见
|
|
15
|
-
* `packages/bundles/session-mode-profile`,`config.plugins` 引用 `@morlay/dsh-agent-toolkit/rows` 的
|
|
16
|
-
* `TOOLKIT_PRESET_ROWS`)。
|
|
17
|
-
*
|
|
18
|
-
* 为什么自己注册而不是复用官方 preset:官方 `minimal` 没有 `tool-web`,`chat` 的白名单(提问 + 联网三件)
|
|
19
|
-
* 收口后一件都不剩;而且官方 preset 自带的上游注入要在我们的开关之外做"让位",抢 `skill` 面还得把目录 kind
|
|
20
|
-
* 换掉。行清单归我们之后,两个模式的**差异全在会话级收口**(persona / `allowTools` / 两个开关)。
|
|
21
|
-
*/
|
|
22
|
-
declare const MODE_PRESET_ID = "mode-switch";
|
|
23
|
-
//#endregion
|
|
24
1
|
//#region src/rows.d.ts
|
|
25
|
-
/** 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。 */
|
|
26
2
|
interface PatchRow {
|
|
27
3
|
readonly insert?: readonly RowEntry[];
|
|
28
4
|
}
|
|
29
|
-
/** `insert` 里的一个条目。 */
|
|
30
5
|
interface RowEntry {
|
|
31
6
|
readonly id: string;
|
|
32
7
|
readonly name: string;
|
|
33
8
|
readonly config?: Readonly<Record<string, unknown>>;
|
|
34
9
|
}
|
|
35
|
-
/** `session-mode` 行的装配:默认模式与各模式的扩展定义。 */
|
|
36
10
|
declare function sessionModeRows(): readonly PatchRow[];
|
|
37
11
|
//#endregion
|
|
38
|
-
export {
|
|
12
|
+
export { PatchRow, RowEntry, sessionModeRows };
|
package/dist/rows.d.mts
CHANGED
|
@@ -1,38 +1,12 @@
|
|
|
1
|
-
//#region src/mode-sources.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* 模式定义的真源:`session-mode` 行的 `config` 由 `./rows.ts` 渲染(装配入口在 `packages/bundles/session-mode-profile`)。
|
|
4
|
-
*
|
|
5
|
-
* "自定义"就落在这份数据上——装配层(profile 的用户 patch 层)可以整体改写 `config.modes`,也可以只给
|
|
6
|
-
* 某个模式换提示词或工具白名单,不需要任何插件行。
|
|
7
|
-
*
|
|
8
|
-
* `allowTools` 从 [`@morlay/dsh-agent-toolkit/rows`](../../../dsh-agent-toolkit/src/rows.ts) 的
|
|
9
|
-
* `TOOLKIT_TOOL_NAMES` 派生(工具名与汉化同源):**行清单由本部署自己注册的那份 preset 提供**
|
|
10
|
-
* ({@link MODE_PRESET_ID},两个模式共享它),白名单里那份 preset 没有的工具自动跳过。想改某一个模式的名单,
|
|
11
|
-
* 直接在这条源数据里加/减。
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* 本部署自己注册的那份 agent preset 的 id:两个模式共享它(行清单见
|
|
15
|
-
* `packages/bundles/session-mode-profile`,`config.plugins` 引用 `@morlay/dsh-agent-toolkit/rows` 的
|
|
16
|
-
* `TOOLKIT_PRESET_ROWS`)。
|
|
17
|
-
*
|
|
18
|
-
* 为什么自己注册而不是复用官方 preset:官方 `minimal` 没有 `tool-web`,`chat` 的白名单(提问 + 联网三件)
|
|
19
|
-
* 收口后一件都不剩;而且官方 preset 自带的上游注入要在我们的开关之外做"让位",抢 `skill` 面还得把目录 kind
|
|
20
|
-
* 换掉。行清单归我们之后,两个模式的**差异全在会话级收口**(persona / `allowTools` / 两个开关)。
|
|
21
|
-
*/
|
|
22
|
-
declare const MODE_PRESET_ID = "mode-switch";
|
|
23
|
-
//#endregion
|
|
24
1
|
//#region src/rows.d.ts
|
|
25
|
-
/** 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。 */
|
|
26
2
|
interface PatchRow {
|
|
27
3
|
readonly insert?: readonly RowEntry[];
|
|
28
4
|
}
|
|
29
|
-
/** `insert` 里的一个条目。 */
|
|
30
5
|
interface RowEntry {
|
|
31
6
|
readonly id: string;
|
|
32
7
|
readonly name: string;
|
|
33
8
|
readonly config?: Readonly<Record<string, unknown>>;
|
|
34
9
|
}
|
|
35
|
-
/** `session-mode` 行的装配:默认模式与各模式的扩展定义。 */
|
|
36
10
|
declare function sessionModeRows(): readonly PatchRow[];
|
|
37
11
|
//#endregion
|
|
38
|
-
export {
|
|
12
|
+
export { PatchRow, RowEntry, sessionModeRows };
|