@morlay/dsh-session-mode 0.0.1-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,368 @@
1
+ import { t as SESSION_MODE_PATH } from "./shared-ChCbbJa8.mjs";
2
+ import { Service } from "@deepseek-ai/cordis";
3
+ import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
4
+ import { z } from "zod";
5
+ import z$1 from "@deepseek-ai/schemastery";
6
+ import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
7
+ //#region src/modes.ts
8
+ const personaSchema = z$1.object({
9
+ prefix: z$1.string().default(""),
10
+ suffix: z$1.string().default("")
11
+ });
12
+ /** 角色是个封闭集合:写错的 role 在装配期就拒绝,而不是静默变成"谁都不用"。 */
13
+ const roleSchema = z$1.union([z$1.const("main"), z$1.const("subagent")]);
14
+ /** 默认模型的形状与全局 `agent-default-model` 一致;省略 effort 就跟 provider 默认。 */
15
+ const modelSchema = z$1.object({
16
+ provider: z$1.string().required(),
17
+ model: z$1.string().required(),
18
+ reasoningEffort: z$1.string()
19
+ });
20
+ const modeSchema = z$1.object({
21
+ name: z$1.string().required(),
22
+ description: z$1.string().default(""),
23
+ role: z$1.array(roleSchema).default(["main"]),
24
+ persona: personaSchema.default({}),
25
+ allowTools: z$1.array(z$1.string()).default([]),
26
+ instructions: z$1.boolean().default(true),
27
+ runtimeContext: z$1.boolean().default(true)
28
+ });
29
+ const Config = z$1.object({
30
+ default: z$1.string().required(),
31
+ modes: z$1.dict(modeSchema).required(),
32
+ models: z$1.dict(modelSchema).default({}).volatile()
33
+ });
34
+ /** 模式定义里不合法的地方(装配期 fail loud,而不是等到某个会话装配提示词时才发现)。 */
35
+ function configProblem(config) {
36
+ /** 没写 `role` 等于默认 `["main"]`(与 schema 的默认一致)——字面量与归一化后的形状都能校验。 */
37
+ const roles = (id) => config.modes[id]?.role ?? ["main"];
38
+ const ids = Object.keys(config.modes);
39
+ if (ids.length === 0) return "session-mode: `modes` must declare at least one mode";
40
+ if (!ids.includes(config.default)) return `session-mode: \`default\` names ${JSON.stringify(config.default)}, which is not in modes (${ids.join(", ")})`;
41
+ const roleless = ids.filter((id) => roles(id).length === 0);
42
+ if (roleless.length > 0) return `session-mode: mode(s) ${roleless.join(", ")} declare no \`role\`; declare main and/or subagent instead of leaving it empty`;
43
+ 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`;
44
+ const empty = ids.filter((id) => (config.modes[id]?.allowTools ?? []).length === 0);
45
+ if (empty.length > 0) return `session-mode: mode(s) ${empty.join(", ")} declare no \`allowTools\`; list the tools instead of leaving it empty`;
46
+ const unknown = Object.keys(config.models ?? {}).filter((id) => !ids.includes(id));
47
+ if (unknown.length > 0) return `session-mode: \`models\` names unknown mode(s) ${unknown.join(", ")}; available are ${ids.join(", ")}`;
48
+ const partial = Object.entries(config.models ?? {}).filter(([, model]) => model.provider === void 0 || model.model === void 0 || model.provider.length === 0 || model.model.length === 0).map(([id]) => id);
49
+ if (partial.length > 0) return `session-mode: mode(s) ${partial.join(", ")} declare \`models\` without both \`provider\` and \`model\``;
50
+ }
51
+ //#endregion
52
+ //#region src/persona.ts
53
+ const PREFIX_ORDER = "DEPLOYMENT_PERSONA_PREFIX";
54
+ const SUFFIX_ORDER = "DEPLOYMENT_PERSONA_SUFFIX";
55
+ /**
56
+ * 把一段 persona 装到该 agent 的 scope 上。
57
+ * @param agent - 目标 agent(用它的 `ctx` 定作用域)。
58
+ * @param persona - 模式的提示词;缺省时两段都注册成空串,等于遮蔽掉部署级那层。
59
+ * @returns 注销这两个 section 的 disposer(模式切换时先调它)。
60
+ */
61
+ function installPersona(agent, persona) {
62
+ const prompt = agent.ctx.systemPrompt;
63
+ const disposers = [prompt.section({
64
+ name: PERSONA_PREFIX_SECTION,
65
+ order: prompt.getSectionOrder(PREFIX_ORDER),
66
+ text: persona?.prefix ?? ""
67
+ }), prompt.section({
68
+ name: PERSONA_SUFFIX_SECTION,
69
+ order: prompt.getSectionOrder(SUFFIX_ORDER),
70
+ text: persona?.suffix ?? ""
71
+ })];
72
+ return () => {
73
+ for (const dispose of disposers) dispose();
74
+ };
75
+ }
76
+ //#endregion
77
+ //#region src/index.ts
78
+ /**
79
+ * 会话模式的 host 半:模式清单(config 里的纯数据)、会话 ↔ 模式的选择、按会话应用 persona 与收口。
80
+ *
81
+ * **模式不是 Cordis 子树**。旧的 `@morlay/dsh-agent-preset` 每个模式都是一行
82
+ * `@deepseek-ai/dsh-agent-preset`,`config.plugins` 里装 persona 与 scope 行,靠 preset scope 的父链
83
+ * 对会话生效——代价是整个官方 registry(声明式行、每 revision 一棵 Loader 子树、`isolate` realm)都在
84
+ * 部署里。这里只留两件事:模式是一份数据,应用落在会话自己的 scope 上:
85
+ *
86
+ * | 事实 | 落在哪 |
87
+ * | ---------------- | ------------------------------------------------------------------------------------------ |
88
+ * | 模式清单与默认值 | 本行的 `config`(装配层可整体改写;`modes.ts` 给形状与校验) |
89
+ * | 各模式的默认模型 | 同一份 config 的顶层 `models`(**volatile**:设置页那张卡片改的就是它) |
90
+ * | 会话当前模式 | session 事件 `session-mode/selected` + 投影 `sessionMode`(log-only,重建读投影) |
91
+ * | 提示词 | `persona`:把模式的 persona 注册到该 agent 的 scope(`persona.ts`) |
92
+ * | 工具与注入开关 | 推给 `ctx.sessionToolScope`(`@morlay/dsh-context-assembler/scope`,行 id `context-assembler-scope`) |
93
+ * | 页面上的选择面 | HTTP 路由 `GET/POST /session-mode`(清单与切换)+ 会话投影(当前值) |
94
+ *
95
+ * 应用时机是 `agent/created`:它早于任何一次提示词装配(装配发生在 turn 里),所以 persona 一定在该会话
96
+ * 第一次装配之前就注册好了。模式在**空白会话**里可以切换,切换时对着已有的 agent 重新应用一遍。
97
+ *
98
+ * 切换只允许在空白窗口(还没开过 turn):会话的历史是在某个模式的工具集与提示词下产生的,换了模式,那段
99
+ * 历史就与实际装配对不上——与上游 `agentPresets.select` 的判据一致,也用同一个投影(`turnBoundary`)。
100
+ */
101
+ /** Cordis 插件名:与行 id 一致。 */
102
+ const name = "session-mode";
103
+ /**
104
+ * 依赖:投影服务(登记 `sessionMode`、读 `turnBoundary`)、读会话与 agent 的两个注册表,以及提示词注册表
105
+ * (persona 的 section 注册在它上面)。
106
+ *
107
+ * 这些名字必须在 `inject` 里点名:cordis 的**属性访问**(`this.ctx.sessions`)要求本 fiber 声明过那个服务,
108
+ * 未声明会抛 `cannot get property "sessions" without inject`(`ctx.get(name)` 才不需要声明)。
109
+ */
110
+ const inject = [
111
+ "agents",
112
+ "sessions",
113
+ "sessionProjections",
114
+ "systemPrompt"
115
+ ];
116
+ const sessionModeSchema = z.union([z.string(), z.null()]);
117
+ /** 会话模式的投影:初值来自空日志(没选过就是 `null`),只被选择事件推进。 */
118
+ const sessionModeProjection = {
119
+ key: "sessionMode",
120
+ stateSchema: sessionModeSchema,
121
+ init: () => null,
122
+ apply: (state, event) => event.type === "session-mode/selected" ? event.data.sessionMode : state,
123
+ wire: {
124
+ viewSchema: sessionModeSchema,
125
+ view: (state) => state
126
+ },
127
+ stateVersion: 1
128
+ };
129
+ /** 模式清单、默认模式、按会话读取与切换。 */
130
+ var SessionModes = class extends Service {
131
+ config;
132
+ /** 每个 agent 已经装上的那一份(persona + 默认模型兜底;模式变了就换一份)。 */
133
+ installs = /* @__PURE__ */ new WeakMap();
134
+ constructor(ctx, config) {
135
+ super(ctx, "sessionModes");
136
+ this.config = config;
137
+ const problem = configProblem({
138
+ ...config,
139
+ models: config.models.get()
140
+ });
141
+ if (problem !== void 0) throw new Error(problem);
142
+ ctx.sessionProjections.register(sessionModeProjection);
143
+ ctx.on("agent/created", ({ agent }) => {
144
+ this.installFor(agent);
145
+ });
146
+ }
147
+ /** 新会话用它:config 里的 `default`。 */
148
+ get defaultId() {
149
+ return this.config.default;
150
+ }
151
+ /** 选择器要的清单:只列 `main` 角色的模式(id、展示名、说明,顺序即 config 里 `modes` 的插入序)。 */
152
+ list() {
153
+ return this.idsFor("main").map((id) => {
154
+ const mode = this.definition(id);
155
+ return {
156
+ id,
157
+ name: mode.name,
158
+ ...mode.description === "" ? {} : { description: mode.description }
159
+ };
160
+ });
161
+ }
162
+ /**
163
+ * 声明了某个角色的模式 id(顺序即 config 的插入序):`main` 给用户选择器,`subagent` 给子代理候选。
164
+ * @param role - 目标角色。
165
+ * @returns 该角色下的模式 id。
166
+ */
167
+ idsFor(role) {
168
+ return Object.entries(this.config.modes).filter(([, mode]) => mode.role.includes(role)).map(([id]) => id);
169
+ }
170
+ /**
171
+ * 同 {@link idsFor},给的是定义——"指定 mode" 那条接缝要拿候选集。
172
+ * @param role - 目标角色。
173
+ * @returns 该角色下的模式与其定义。
174
+ */
175
+ modesFor(role) {
176
+ return this.idsFor(role).map((id) => ({
177
+ id,
178
+ mode: this.definition(id)
179
+ }));
180
+ }
181
+ /** 页面用的清单 + 默认模式。 */
182
+ roster() {
183
+ return {
184
+ default: this.defaultId,
185
+ modes: this.list()
186
+ };
187
+ }
188
+ /** 按 id 取定义;未知 id 直接抛(切换路径上它就是用户的错)。 */
189
+ definition(id) {
190
+ const wanted = id ?? this.defaultId;
191
+ const mode = this.config.modes[wanted];
192
+ if (mode === void 0) throw new Error(`未知的模式 ${JSON.stringify(wanted)};可用的是 ${Object.keys(this.config.modes).join(", ")}`);
193
+ return mode;
194
+ }
195
+ /** 会话当前模式:投影上有就用它,否则是部署默认。 */
196
+ modeOf(session) {
197
+ return this.ctx.sessionProjections.stateOf(session, "sessionMode") ?? this.defaultId;
198
+ }
199
+ /** 会话当前模式的定义。 */
200
+ modeOfSession(session) {
201
+ return this.definition(this.modeOf(session));
202
+ }
203
+ /**
204
+ * 把某个空白会话切到某个模式。
205
+ * @param sessionId - 目标会话(必须还没有开过 turn)。
206
+ * @param mode - 目标模式 id。
207
+ * @returns 提交后的模式 id。
208
+ */
209
+ async select(sessionId, mode) {
210
+ if (!this.definition(mode).role.includes("main")) throw new Error(`模式 ${JSON.stringify(mode)} 不是用户可选的(它的 role 里没有 main)。`);
211
+ const session = this.ctx.sessions.get(sessionId);
212
+ if (session === void 0) throw new Error(`未知的会话 ${sessionId}`);
213
+ const boundary = this.ctx.sessionProjections.stateOf(session, "turnBoundary");
214
+ if (boundary !== void 0 && (boundary.openTurnStartSeq !== null || boundary.lastTurn > 0)) throw new Error("这个会话已经开始,模式不能再改;要换模式请新开一个会话。");
215
+ session.append("session-mode/selected", { sessionMode: mode });
216
+ const agent = this.ctx.agents.get(sessionId);
217
+ if (agent !== void 0) this.installFor(agent);
218
+ return mode;
219
+ }
220
+ /**
221
+ * 把某个活着的 agent 切到某个模式。与 {@link select} 的差别:它不要求空白会话——子代理创建时的继承
222
+ * 走这里,**未来的"指定 mode"入口(模型侧或配置侧)也走这里**(那条接缝还没做)。
223
+ * @param agent - 目标 agent。
224
+ * @param mode - 目标模式 id。
225
+ * @param options.record - 是否把这次切换写进会话日志(缺省写;只想改当前进程时给 `false`)。
226
+ */
227
+ applyTo(agent, mode, options = {}) {
228
+ this.definition(mode);
229
+ if (options.record !== false) agent.session.append("session-mode/selected", { sessionMode: mode });
230
+ this.installFor(agent, mode);
231
+ }
232
+ /**
233
+ * 该 agent 用哪个模式:会话选过(投影上有)优先,子代理继承父,其余用部署默认。
234
+ *
235
+ * 继承要**写进子会话日志**:它是一条会话事实,冷恢复与 fork 都要靠它重建({@link modeOf} 只读投影)。
236
+ */
237
+ resolveModeId(agent) {
238
+ const selected = this.ctx.sessionProjections.stateOf(agent.session, "sessionMode");
239
+ if (typeof selected === "string") return selected;
240
+ const inherited = this.inheritedModeId(agent);
241
+ if (inherited === void 0) return this.defaultId;
242
+ agent.session.append("session-mode/selected", { sessionMode: inherited });
243
+ return inherited;
244
+ }
245
+ /** 子代理(有 durable 父会话)继承父当前模式;父不在场、或不是子代理时没有可继承的。 */
246
+ inheritedModeId(agent) {
247
+ const parentId = agent.session.header.parentSession;
248
+ if (parentId === void 0) return void 0;
249
+ const parent = this.ctx.agents.get(parentId);
250
+ return parent === void 0 ? void 0 : this.modeOf(parent.session);
251
+ }
252
+ /** 装或换该 agent 的那一份(幂等:同一模式不重复注册)。 */
253
+ installFor(agent, modeId = this.resolveModeId(agent)) {
254
+ const installed = this.installs.get(agent);
255
+ if (installed?.mode === modeId) return;
256
+ installed?.dispose();
257
+ const mode = this.definition(modeId);
258
+ const disposers = [installPersona(agent, mode.persona), this.installDefaultModel(agent, modeId)];
259
+ this.installs.set(agent, {
260
+ mode: modeId,
261
+ dispose: () => {
262
+ for (const dispose of disposers) dispose();
263
+ }
264
+ });
265
+ this.toolScope()?.apply(agent, mode);
266
+ }
267
+ /**
268
+ * 模式的默认模型(config 顶层 `models.<模式 id>`)兜底:只在会话**尚无任何模型事实**(没选过模型、也
269
+ * 还没跑过请求)时接管这一请求的路由;一旦用户选过(投影 `pending`)或会话已经落过 header,就不再插手。
270
+ *
271
+ * 它是**配置事实**,不写会话事件——重启后仍由 config 决定;设置页里那条会话级选择才是会话事实。
272
+ *
273
+ * 模型取自 `config.models`:那是个 volatile 引用,设置页保存时只有**引用里的值**变,这一行不重挂,
274
+ * 所以每次请求都现场 `.get()`(与 `llm-openai-compatible` 读 `config.providers` 同一种读法)。
275
+ */
276
+ installDefaultModel(agent, modeId) {
277
+ return agent.ctx.on("agent/request", async (_payload, next) => {
278
+ const resolved = await next();
279
+ const model = this.config.models.get()[modeId];
280
+ if (model === void 0) return resolved;
281
+ const pending = this.ctx.sessionProjections.stateOf(agent.session, "modelSelection")?.pending;
282
+ if (pending !== void 0 && pending !== null) return resolved;
283
+ if (agent.session.requestHeader() !== void 0) return resolved;
284
+ return {
285
+ ...resolved,
286
+ provider: model.provider,
287
+ model: model.model,
288
+ ...model.reasoningEffort === void 0 ? {} : { reasoningEffort: ReasoningEffortId(model.reasoningEffort) }
289
+ };
290
+ });
291
+ }
292
+ /** 收口服务由 `@morlay/dsh-context-assembler/scope` 那一行发布;没装它就只有 persona。 */
293
+ toolScope() {
294
+ try {
295
+ return this.ctx.get("sessionToolScope");
296
+ } catch {
297
+ return;
298
+ }
299
+ }
300
+ };
301
+ function apply(ctx, config) {
302
+ registerHttpRoutes(ctx, new SessionModes(ctx, config));
303
+ }
304
+ function respondJson(response, status, body) {
305
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
306
+ response.end(JSON.stringify(body));
307
+ }
308
+ function requestJson(request) {
309
+ return new Promise((resolve, reject) => {
310
+ const chunks = [];
311
+ request.on("data", (chunk) => {
312
+ chunks.push(typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk));
313
+ });
314
+ request.on("end", () => {
315
+ try {
316
+ resolve(chunks.length === 0 ? void 0 : JSON.parse(chunks.join("")));
317
+ } catch {
318
+ reject(/* @__PURE__ */ new TypeError("请求体不是合法 JSON。"));
319
+ }
320
+ });
321
+ request.on("error", reject);
322
+ });
323
+ }
324
+ function requiredString(value, field) {
325
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`${field} 必须是非空字符串。`);
326
+ return value;
327
+ }
328
+ async function handleRoute(modes, request, response) {
329
+ try {
330
+ if (request.method === "GET") {
331
+ respondJson(response, 200, modes.roster());
332
+ return;
333
+ }
334
+ if (request.method === "POST") {
335
+ const body = await requestJson(request);
336
+ if (typeof body !== "object" || body === null || Array.isArray(body)) throw new TypeError("请求体必须是 JSON 对象。");
337
+ const record = body;
338
+ const sessionId = requiredString(record["sessionId"], "sessionId");
339
+ const mode = requiredString(record["mode"], "mode");
340
+ respondJson(response, 200, { mode: await modes.select(sessionId, mode) });
341
+ return;
342
+ }
343
+ response.writeHead(405);
344
+ response.end();
345
+ } catch (error) {
346
+ const message = error instanceof Error ? error.message : String(error);
347
+ respondJson(response, error instanceof TypeError ? 400 : 409, { error: message });
348
+ }
349
+ }
350
+ /**
351
+ * 把清单与切换挂到同一张宿主路由表上。
352
+ *
353
+ * `webServer` **必须等**:它可能比本行晚激活,而一次性 `ctx.get` 取到 `undefined` 之后不会再试一次——
354
+ * 路由没注册的后果是请求落到静态资源 fallback,非 GET/HEAD 一律 405。
355
+ */
356
+ function registerHttpRoutes(ctx, modes) {
357
+ ctx.inject(["webServer"], (scope) => {
358
+ const webServer = scope.get("webServer");
359
+ if (webServer === void 0) return;
360
+ scope.effect(() => webServer.register({
361
+ kind: "exact",
362
+ path: SESSION_MODE_PATH,
363
+ handler: (request, response) => handleRoute(modes, request, response)
364
+ }), "session-mode: HTTP route");
365
+ });
366
+ }
367
+ //#endregion
368
+ export { Config, SESSION_MODE_PATH, SessionModes, apply, inject, name, sessionModeProjection };
@@ -0,0 +1,14 @@
1
+ //#region src/shared.ts
2
+ /**
3
+ * host 半与 client 半共用的接口面:一条 HTTP 路径、模式行的对外形状、请求与响应体。
4
+ *
5
+ * 为什么不是 Typert Remote:客户端的 remote 清单(`@deepseek-ai/dsh-api-remotes/client`)由上游硬编码,
6
+ * 我们的服务不在里面,`ctx.remote.sessionModes` 解析不到。仓库既有的跨半通路是 HTTP 路由
7
+ * (见 `@morlay/ui-conversation-message-actions` 的 `/session-editor`),这里沿用同一种。
8
+ *
9
+ * 会话当前模式不走这条通路:它是一条 session 投影(`sessionMode`),随会话列表一起到页面。
10
+ */
11
+ /** 模式清单与切换的路由路径:宿主(web 与桌面)在同一张路由表上服务它。 */
12
+ const SESSION_MODE_PATH = "/session-mode";
13
+ //#endregion
14
+ export { SESSION_MODE_PATH as t };
@@ -0,0 +1,106 @@
1
+ import { Volatile } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ //#region src/modes.d.ts
4
+ /** 一个模式的提示词:两段文本,注册成 agent 作用域的 `deployment:persona-prefix` / `-suffix` section。 */
5
+ interface SessionModePersona {
6
+ /** 系统提示词最前的一段;空串表示不遮蔽部署级那层。 */
7
+ readonly prefix: string;
8
+ /** 系统提示词最后的一段;空串表示不写。 */
9
+ readonly suffix: string;
10
+ }
11
+ /**
12
+ * 一个模式对谁可见:`main` 进用户选择器(会话级选择),`subagent` 表示它**可以**作为子代理的 mode。
13
+ * 两个角色可以同时声明;不写默认 `["main"]`——没写角色的模式不该悄悄变成子代理候选。
14
+ *
15
+ * `subagent` 目前只是候选集的声明:子代理默认继承父 mode(不看角色),"按角色指派 mode" 还没做。
16
+ */
17
+ type SessionModeRole = "main" | "subagent";
18
+ /** 一个模式的默认模型;省略的字段跟着 provider 默认走(与全局 `agent-default-model` 同形状)。 */
19
+ interface SessionModeModel {
20
+ readonly provider: string;
21
+ readonly model: string;
22
+ readonly reasoningEffort?: string;
23
+ }
24
+ /** 各模式的默认模型:模式 id → 模型;键必须在 `modes` 里(装配期校验)。 */
25
+ type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
26
+ /**
27
+ * 一个模式:提示词 + 能力开关。
28
+ *
29
+ * 这份形状是 **schema 归一化之后**的:每个字段都有值(写配置时可以不写,schema 用默认补上——`description`
30
+ * 补空串、`persona` 补两段空文本、两个布尔开关补 `true`)。配置里能省略哪些字段看 `modeSchema` 的 default,
31
+ * 不看这里。
32
+ */
33
+ interface SessionMode {
34
+ /** 选择器里的展示名。 */
35
+ readonly name: string;
36
+ /** 一句话说明这个模式干什么;空串表示没写。 */
37
+ readonly description: string;
38
+ /** 这个模式归谁用:`main`(用户选择器)/ `subagent`(可作子代理 mode)。至少一个。 */
39
+ readonly role: SessionModeRole[];
40
+ /** 该模式的提示词。 */
41
+ readonly persona: SessionModePersona;
42
+ /**
43
+ * 这个模式能用哪些工具;其余工具既不进模型目录、调用也被执行层拒绝,它们自己的说明 section
44
+ * (`tool:<工具名>`)也不留在提示词里。**至少给一个**——想要"全都要"就列出全部,别留空。
45
+ */
46
+ readonly allowTools: string[];
47
+ /**
48
+ * 是否要 instruction 类注入(工作区指令、技能目录、用法正文)。缺省要;`false` 表示这个模式一条都不要
49
+ * ——对话模式就是它。开关由 `@morlay/dsh-context-assembler/scope` 落到通道上。
50
+ */
51
+ readonly instructions: boolean;
52
+ /**
53
+ * 是否要 runtime context(文件沙箱策略、审批策略那两条动态快照)。缺省要;`false` 表示这个模式不要它们
54
+ * ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
55
+ */
56
+ readonly runtimeContext: boolean;
57
+ }
58
+ /** 本包 config 的**源码形状**:装配层与设置页写的那个形状(`models` 是普通对象,可以整块省略)。 */
59
+ interface Config {
60
+ /** 新会话(还没选过模式的会话)用哪个模式。必须是 `modes` 里的一个 id。 */
61
+ readonly default: string;
62
+ /** 模式清单:id → 定义。顺序即选择器里的顺序(`Object.entries` 的插入序)。 */
63
+ readonly modes: Record<string, SessionMode>;
64
+ /**
65
+ * 各模式的默认模型:模式 id → 模型。省略的模式跟着全局 `agent-default-model` 走。
66
+ *
67
+ * 它是 config 的**顶层 volatile 字段**:设置面(`ctx.configForms` → 我们那张卡片)编辑的就是它。挪进
68
+ * `modes.<id>` 会让设置面看不见它——`schemastery` 的 `validateVolatileSchema` 把 dict 内部一律当成
69
+ * blocked,而 settings 只挑得出固定路径上的 volatile 字段。
70
+ */
71
+ readonly models?: SessionModeModels;
72
+ }
73
+ /** schema 解析之后的形状:volatile 字段被换成**稳定引用**,读它要过 `.get()`(设置页改的就是同一份)。 */
74
+ interface ResolvedConfig {
75
+ readonly default: string;
76
+ readonly modes: Record<string, SessionMode>;
77
+ /** 各模式的默认模型;一个都没配时是空对象(schema 的 default)。 */
78
+ readonly models: Volatile<SessionModeModels>;
79
+ }
80
+ declare const Config: z<Config, ResolvedConfig>;
81
+ //#endregion
82
+ //#region src/shared.d.ts
83
+ /**
84
+ * host 半与 client 半共用的接口面:一条 HTTP 路径、模式行的对外形状、请求与响应体。
85
+ *
86
+ * 为什么不是 Typert Remote:客户端的 remote 清单(`@deepseek-ai/dsh-api-remotes/client`)由上游硬编码,
87
+ * 我们的服务不在里面,`ctx.remote.sessionModes` 解析不到。仓库既有的跨半通路是 HTTP 路由
88
+ * (见 `@morlay/ui-conversation-message-actions` 的 `/session-editor`),这里沿用同一种。
89
+ *
90
+ * 会话当前模式不走这条通路:它是一条 session 投影(`sessionMode`),随会话列表一起到页面。
91
+ */
92
+ /** 模式清单与切换的路由路径:宿主(web 与桌面)在同一张路由表上服务它。 */
93
+ declare const SESSION_MODE_PATH = "/session-mode";
94
+ /** 一个模式对外的那部分:选择器要的名字与说明。 */
95
+ interface SessionModeRow {
96
+ readonly id: string;
97
+ readonly name: string;
98
+ readonly description?: string;
99
+ }
100
+ /** `GET` 的响应体:清单与默认模式。 */
101
+ interface SessionModeRoster {
102
+ readonly default: string;
103
+ readonly modes: readonly SessionModeRow[];
104
+ }
105
+ //#endregion
106
+ export { ResolvedConfig as a, SessionModeModels as c, Config as i, SessionModePersona as l, SessionModeRoster as n, SessionMode as o, SessionModeRow as r, SessionModeModel as s, SESSION_MODE_PATH as t, SessionModeRole as u };
@@ -0,0 +1,106 @@
1
+ import { Volatile } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ //#region src/modes.d.ts
4
+ /** 一个模式的提示词:两段文本,注册成 agent 作用域的 `deployment:persona-prefix` / `-suffix` section。 */
5
+ interface SessionModePersona {
6
+ /** 系统提示词最前的一段;空串表示不遮蔽部署级那层。 */
7
+ readonly prefix: string;
8
+ /** 系统提示词最后的一段;空串表示不写。 */
9
+ readonly suffix: string;
10
+ }
11
+ /**
12
+ * 一个模式对谁可见:`main` 进用户选择器(会话级选择),`subagent` 表示它**可以**作为子代理的 mode。
13
+ * 两个角色可以同时声明;不写默认 `["main"]`——没写角色的模式不该悄悄变成子代理候选。
14
+ *
15
+ * `subagent` 目前只是候选集的声明:子代理默认继承父 mode(不看角色),"按角色指派 mode" 还没做。
16
+ */
17
+ type SessionModeRole = "main" | "subagent";
18
+ /** 一个模式的默认模型;省略的字段跟着 provider 默认走(与全局 `agent-default-model` 同形状)。 */
19
+ interface SessionModeModel {
20
+ readonly provider: string;
21
+ readonly model: string;
22
+ readonly reasoningEffort?: string;
23
+ }
24
+ /** 各模式的默认模型:模式 id → 模型;键必须在 `modes` 里(装配期校验)。 */
25
+ type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
26
+ /**
27
+ * 一个模式:提示词 + 能力开关。
28
+ *
29
+ * 这份形状是 **schema 归一化之后**的:每个字段都有值(写配置时可以不写,schema 用默认补上——`description`
30
+ * 补空串、`persona` 补两段空文本、两个布尔开关补 `true`)。配置里能省略哪些字段看 `modeSchema` 的 default,
31
+ * 不看这里。
32
+ */
33
+ interface SessionMode {
34
+ /** 选择器里的展示名。 */
35
+ readonly name: string;
36
+ /** 一句话说明这个模式干什么;空串表示没写。 */
37
+ readonly description: string;
38
+ /** 这个模式归谁用:`main`(用户选择器)/ `subagent`(可作子代理 mode)。至少一个。 */
39
+ readonly role: SessionModeRole[];
40
+ /** 该模式的提示词。 */
41
+ readonly persona: SessionModePersona;
42
+ /**
43
+ * 这个模式能用哪些工具;其余工具既不进模型目录、调用也被执行层拒绝,它们自己的说明 section
44
+ * (`tool:<工具名>`)也不留在提示词里。**至少给一个**——想要"全都要"就列出全部,别留空。
45
+ */
46
+ readonly allowTools: string[];
47
+ /**
48
+ * 是否要 instruction 类注入(工作区指令、技能目录、用法正文)。缺省要;`false` 表示这个模式一条都不要
49
+ * ——对话模式就是它。开关由 `@morlay/dsh-context-assembler/scope` 落到通道上。
50
+ */
51
+ readonly instructions: boolean;
52
+ /**
53
+ * 是否要 runtime context(文件沙箱策略、审批策略那两条动态快照)。缺省要;`false` 表示这个模式不要它们
54
+ * ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
55
+ */
56
+ readonly runtimeContext: boolean;
57
+ }
58
+ /** 本包 config 的**源码形状**:装配层与设置页写的那个形状(`models` 是普通对象,可以整块省略)。 */
59
+ interface Config {
60
+ /** 新会话(还没选过模式的会话)用哪个模式。必须是 `modes` 里的一个 id。 */
61
+ readonly default: string;
62
+ /** 模式清单:id → 定义。顺序即选择器里的顺序(`Object.entries` 的插入序)。 */
63
+ readonly modes: Record<string, SessionMode>;
64
+ /**
65
+ * 各模式的默认模型:模式 id → 模型。省略的模式跟着全局 `agent-default-model` 走。
66
+ *
67
+ * 它是 config 的**顶层 volatile 字段**:设置面(`ctx.configForms` → 我们那张卡片)编辑的就是它。挪进
68
+ * `modes.<id>` 会让设置面看不见它——`schemastery` 的 `validateVolatileSchema` 把 dict 内部一律当成
69
+ * blocked,而 settings 只挑得出固定路径上的 volatile 字段。
70
+ */
71
+ readonly models?: SessionModeModels;
72
+ }
73
+ /** schema 解析之后的形状:volatile 字段被换成**稳定引用**,读它要过 `.get()`(设置页改的就是同一份)。 */
74
+ interface ResolvedConfig {
75
+ readonly default: string;
76
+ readonly modes: Record<string, SessionMode>;
77
+ /** 各模式的默认模型;一个都没配时是空对象(schema 的 default)。 */
78
+ readonly models: Volatile<SessionModeModels>;
79
+ }
80
+ declare const Config: z<Config, ResolvedConfig>;
81
+ //#endregion
82
+ //#region src/shared.d.ts
83
+ /**
84
+ * host 半与 client 半共用的接口面:一条 HTTP 路径、模式行的对外形状、请求与响应体。
85
+ *
86
+ * 为什么不是 Typert Remote:客户端的 remote 清单(`@deepseek-ai/dsh-api-remotes/client`)由上游硬编码,
87
+ * 我们的服务不在里面,`ctx.remote.sessionModes` 解析不到。仓库既有的跨半通路是 HTTP 路由
88
+ * (见 `@morlay/ui-conversation-message-actions` 的 `/session-editor`),这里沿用同一种。
89
+ *
90
+ * 会话当前模式不走这条通路:它是一条 session 投影(`sessionMode`),随会话列表一起到页面。
91
+ */
92
+ /** 模式清单与切换的路由路径:宿主(web 与桌面)在同一张路由表上服务它。 */
93
+ declare const SESSION_MODE_PATH = "/session-mode";
94
+ /** 一个模式对外的那部分:选择器要的名字与说明。 */
95
+ interface SessionModeRow {
96
+ readonly id: string;
97
+ readonly name: string;
98
+ readonly description?: string;
99
+ }
100
+ /** `GET` 的响应体:清单与默认模式。 */
101
+ interface SessionModeRoster {
102
+ readonly default: string;
103
+ readonly modes: readonly SessionModeRow[];
104
+ }
105
+ //#endregion
106
+ export { ResolvedConfig as a, SessionModeModels as c, Config as i, SessionModePersona as l, SessionModeRoster as n, SessionMode as o, SessionModeRow as r, SessionModeModel as s, SESSION_MODE_PATH as t, SessionModeRole as u };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Session modes",
4
+ "description": "The coding / chat modes as one row of data (persona plus the allowed tools); the host applies the current one per session, and the new-session chip plus the header label replace the official agent-preset surface."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "会话模式",
4
+ "description": "coding / chat 两个模式:一行数据(提示词 + 工具白名单),host 按会话应用;新会话的顶部 chip 与会话头部标签替代官方的 agent preset 面。"
5
+ }
6
+ }