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

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.d.cts CHANGED
@@ -1,8 +1,127 @@
1
- import { a as ResolvedConfig, c as SessionModeModels, i as Config, l as SessionModePersona, n as SessionModeRoster, o as SessionMode, r as SessionModeRow, s as SessionModeModel, t as SESSION_MODE_PATH, u as SessionModeRole } from "./shared-FNWe-_rS.cjs";
2
- import { Context, Service } from "@deepseek-ai/cordis";
1
+ import { Context, Service, Volatile } from "@deepseek-ai/cordis";
3
2
  import { Agent } from "@deepseek-ai/dsh-agent";
4
3
  import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
5
4
  import { z } from "zod";
5
+ import z$1 from "@deepseek-ai/schemastery";
6
+ //#region src/modes.d.ts
7
+ /** 一个模式的提示词:两段文本,注册成 agent 作用域的 `deployment:persona-prefix` / `-suffix` section。 */
8
+ interface SessionModePersona {
9
+ /** 系统提示词最前的一段;空串表示不遮蔽部署级那层。 */
10
+ readonly prefix: string;
11
+ /** 系统提示词最后的一段;空串表示不写。 */
12
+ readonly suffix: string;
13
+ }
14
+ /**
15
+ * 一个模式对谁可见:`main` 进用户选择器(会话级选择),`subagent` 表示它**可以**作为子代理的 mode。
16
+ * 两个角色可以同时声明;不写默认 `["main"]`——没写角色的模式不该悄悄变成子代理候选。
17
+ *
18
+ * `subagent` 目前只是候选集的声明:子代理默认继承父 mode(不看角色),"按角色指派 mode" 还没做。
19
+ */
20
+ type SessionModeRole = "main" | "subagent";
21
+ /** 一个模式的默认模型;省略的字段跟着 provider 默认走(与全局 `agent-default-model` 同形状)。 */
22
+ interface SessionModeModel {
23
+ readonly provider: string;
24
+ readonly model: string;
25
+ readonly reasoningEffort?: string;
26
+ }
27
+ /** 退役的顶层形状:模式 id → 模型(默认模型现在住在各自的模式里)。 */
28
+ type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
29
+ /**
30
+ * 一个模式:提示词 + 能力开关。
31
+ *
32
+ * 这份形状是 **schema 归一化之后**的:每个字段都有值(写配置时可以不写,schema 用默认补上——`description`
33
+ * 补空串、`persona` 补两段空文本、两个布尔开关补 `true`)。配置里能省略哪些字段看 `modeSchema` 的 default,
34
+ * 不看这里。
35
+ */
36
+ interface SessionMode {
37
+ /**
38
+ * 这个模式挂在哪个 **agent preset** 上(官方 `standard` / `ptc` / `minimal` / `cordis`,或本部署自己
39
+ * 注册的那一份的 `id`,见 `mode-sources.ts` 的 `MODE_PRESET_ID`)。
40
+ *
41
+ * preset 决定这个 agent 有哪些行(工具 / 命令 / 压缩 / 委派…),模式只决定"这些行怎么被用":
42
+ * `allowTools` 里 preset 没有的工具自动跳过,其余扩展(persona / 注入开关 / 默认模型)照常应用。
43
+ * **可以共享**(本部署就是两个模式挂同一份 preset,差异全在会话级收口);共享时 preset → 模式的反查无从下手,
44
+ * 见 `SessionModes.modeForPreset`。空串表示不挂(只能由 applyTo / 继承使用)。
45
+ */
46
+ readonly preset: string;
47
+ /** 模式的展示名(选择面归官方 roster;这里留着做事实文案)。 */
48
+ readonly name: string;
49
+ /** 一句话说明这个模式干什么;空串表示没写。 */
50
+ readonly description: string;
51
+ /** 这个模式归谁用:`main`(用户选择器)/ `subagent`(可作子代理 mode)。至少一个。 */
52
+ readonly role: SessionModeRole[];
53
+ /** 该模式的提示词。 */
54
+ readonly persona: SessionModePersona;
55
+ /**
56
+ * 这个模式能用哪些工具;其余工具既不进模型目录、调用也被执行层拒绝,它们自己的说明 section
57
+ * (`tool:<工具名>`)也不留在提示词里。**至少给一个**——想要"全都要"就列出全部,别留空。
58
+ */
59
+ readonly allowTools: string[];
60
+ /**
61
+ * 是否要 instruction 类注入(工作区指令、技能目录、用法正文)。缺省要;`false` 表示这个模式一条都不要
62
+ * ——对话模式就是它。开关由 `@morlay/dsh-context-assembler/scope` 落到通道上。
63
+ */
64
+ readonly instructions: boolean;
65
+ /**
66
+ * 是否要 runtime context(文件沙箱策略、审批策略那两条动态快照)。缺省要;`false` 表示这个模式不要它们
67
+ * ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
68
+ */
69
+ readonly runtimeContext: boolean;
70
+ /**
71
+ * 这个模式的默认模型;省略就跟全局 `agent-default-model`。**可选**:没配的模式在页面上不出现在这一行
72
+ * (`defaultModel` 是它所在模式的一个可加字段)。
73
+ */
74
+ readonly defaultModel?: SessionModeModel;
75
+ }
76
+ /** 本包 config 的**源码形状**:装配层与设置页写的那个形状。 */
77
+ interface Config {
78
+ /** 新会话(还没选过模式的会话)用哪个模式。必须是 `modes` 里的一个 id。 */
79
+ readonly default: string;
80
+ /** 模式清单:id → 定义(含各自的 `defaultModel`)。顺序即选择器里的顺序(`Object.entries` 的插入序)。 */
81
+ readonly modes: Record<string, SessionMode>;
82
+ /**
83
+ * 各模式默认模型曾经住在这里(模式 id → 模型)。现在住在**每个模式自己的 `defaultModel`** 里,这个字段
84
+ * 只剩一件事:装配期看见它还配着值就报错,提醒把它挪进对应的模式——否则它会静静地失效。
85
+ */
86
+ readonly models?: SessionModeModels;
87
+ }
88
+ /**
89
+ * schema 解析之后的形状:volatile 字段被换成**稳定引用**,读它要过 `.get()`(设置页改的就是同一份)。
90
+ *
91
+ * `default` 与 `modes` 都是 volatile:默认模式与整份模式清单(含各自的 `defaultModel`)都在行配置页上。
92
+ */
93
+ interface ResolvedConfig {
94
+ readonly default: Volatile<string>;
95
+ readonly modes: Volatile<Record<string, SessionMode>>;
96
+ /** 退役的顶层字段:解析后仍在这儿(普通值,不 volatile),装配期据此发现"还配着值"并报错。 */
97
+ readonly models: SessionModeModels;
98
+ }
99
+ declare const Config: z$1<Config, ResolvedConfig>;
100
+ //#endregion
101
+ //#region src/shared.d.ts
102
+ /**
103
+ * host 半与 client 半共用的接口面:一条 HTTP 路径、模式行的对外形状、请求与响应体。
104
+ *
105
+ * 为什么不是 Typert Remote:客户端的 remote 清单(`@deepseek-ai/dsh-api-remotes/client`)由上游硬编码,
106
+ * 我们的服务不在里面,`ctx.remote.sessionModes` 解析不到。仓库既有的跨半通路是 HTTP 路由
107
+ * (见 `@morlay/ui-conversation-message-actions` 的 `/session-editor`),这里沿用同一种。
108
+ *
109
+ * 会话当前模式不走这条通路:它是一条 session 投影(`sessionMode`),随会话列表一起到页面。
110
+ */
111
+ /** 模式清单与切换的路由路径:宿主(web 与桌面)在同一张路由表上服务它。 */
112
+ declare const SESSION_MODE_PATH = "/session-mode";
113
+ /** 一个模式对外的那部分:选择器要的名字与说明。 */
114
+ interface SessionModeRow {
115
+ readonly id: string;
116
+ readonly name: string;
117
+ readonly description?: string;
118
+ }
119
+ /** `GET` 的响应体:清单与默认模式。 */
120
+ interface SessionModeRoster {
121
+ readonly default: string;
122
+ readonly modes: readonly SessionModeRow[];
123
+ }
124
+ //#endregion
6
125
  //#region src/index.d.ts
7
126
  /** Cordis 插件名:与行 id 一致。 */
8
127
  declare const name = "session-mode";
@@ -33,10 +152,13 @@ declare module "@deepseek-ai/dsh-session/types" {
33
152
  declare module "@deepseek-ai/dsh-session-projection/types" {
34
153
  interface SessionProjectionStateMap {
35
154
  sessionMode: string | null;
155
+ sessionModeEditable: boolean;
36
156
  }
37
157
  interface SessionProjectionMap {
38
158
  /** 会话当前模式;`null` 表示没选过(用部署默认)。 */
39
159
  sessionMode: string | null;
160
+ /** 会话还能不能换模式:`true` 是选择器,`false` 是只读标签(client 那个 chip 据此变形)。 */
161
+ sessionModeEditable: boolean;
40
162
  }
41
163
  }
42
164
  /** 会话模式的投影:初值来自空日志(没选过就是 `null`),只被选择事件推进。 */
@@ -51,11 +173,33 @@ declare const sessionModeProjection: {
51
173
  };
52
174
  stateVersion: number;
53
175
  };
176
+ /**
177
+ * 这个会话能不能换模式的投影:空白会话为 `true`,一旦 `turn/start` 落库就永远 `false`(换模式要的是**整段
178
+ * 历史**的模式一致,所以"开过 turn"之后连正在跑的那个 turn 也算)。
179
+ *
180
+ * **判据只有这一处**:服务端 {@link SessionModes.select} 的拒绝与 client chip 的只读形态都读它——以前
181
+ * client 只能等服务端报错,现在连入口都不给,而两边的结论来自同一份会话事实。
182
+ */
183
+ declare const sessionModeEditableProjection: {
184
+ key: "sessionModeEditable";
185
+ stateSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
186
+ init: () => true;
187
+ apply: (state: boolean, event: SessionEvent) => boolean;
188
+ wire: {
189
+ viewSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
190
+ view: (state: boolean) => boolean;
191
+ };
192
+ stateVersion: number;
193
+ };
54
194
  /** 模式清单、默认模式、按会话读取与切换。 */
55
195
  declare class SessionModes extends Service {
56
- config: ResolvedConfig;
57
196
  /** 每个 agent 已经装上的那一份(persona + 默认模型兜底;模式变了就换一份)。 */
58
197
  private readonly installs;
198
+ /** 装配时的配置快照:`default` / `modes` 读它,改这两项靠 Loader 重挂这一行(已运行会话不自动换定义)。 */
199
+ readonly config: {
200
+ default: string;
201
+ modes: Record<string, SessionMode>;
202
+ };
59
203
  constructor(ctx: Context, config: ResolvedConfig);
60
204
  /** 新会话用它:config 里的 `default`。 */
61
205
  get defaultId(): string;
@@ -86,11 +230,30 @@ declare class SessionModes extends Service {
86
230
  modeOfSession(session: Session): SessionMode;
87
231
  /**
88
232
  * 把某个空白会话切到某个模式。
233
+ *
234
+ * 模式带着它的 preset(`preset` 决定行清单),所以这里**先把 agent preset 换成模式声明的那个**,再落我们的
235
+ * 会话事实:否则 preset realm 还是旧那一套的行,而它的注入在我们的开关之外(旧形态 `chat` 挂 `minimal`、
236
+ * 会话的 preset 却还是 `standard` 时,上游 `agent-instructions` 会照旧把工作区指令注进这个"不要注入"的会话)。
237
+ *
238
+ * 目标 preset 与当前挂着的**相同时不切**(本部署两个模式共享同一份 preset,所以切模式通常走不到这一步):
239
+ * 换 preset 是一次重挂(卸旧行、装新行),没有变化就没有理由付出这个代价。
89
240
  * @param sessionId - 目标会话(必须还没有开过 turn)。
90
241
  * @param mode - 目标模式 id。
91
242
  * @returns 提交后的模式 id。
92
243
  */
93
244
  select(sessionId: SessionId, mode: string): Promise<string>;
245
+ /**
246
+ * 某个 agent 当前挂着的 preset(registry 的 `composedPreset`)。
247
+ *
248
+ * 读不到时返回 `undefined`(按"未知"处理 → 该切就切):`composedPreset` 是 registry 较新的读面,替身与老
249
+ * 版本可能没有它;而"没挂任何 preset"(返回值 `undefined`)与"读不到"在这里是同一个结论。
250
+ */
251
+ private presetOfAgent;
252
+ /**
253
+ * 官方 preset registry:行清单与它的选择面住在那一行。不在 `inject` 里点名(headless 部署没有它,
254
+ * 点名会让本行永不激活),所以按"可能拿不到"读——`ctx.get` 在当前 ctx 没声明那个服务时会抛。
255
+ */
256
+ private presetRegistry;
94
257
  /**
95
258
  * 把某个活着的 agent 切到某个模式。与 {@link select} 的差别:它不要求空白会话——子代理创建时的继承
96
259
  * 走这里,**未来的"指定 mode"入口(模型侧或配置侧)也走这里**(那条接缝还没做)。
@@ -107,18 +270,29 @@ declare class SessionModes extends Service {
107
270
  * 继承要**写进子会话日志**:它是一条会话事实,冷恢复与 fork 都要靠它重建({@link modeOf} 只读投影)。
108
271
  */
109
272
  private resolveModeId;
273
+ /** 官方 roster 选定的 preset(没选过、或 registry 没装时为 `undefined`)。 */
274
+ private presetOf;
275
+ /**
276
+ * 某个 agent preset 对应的模式 id——**只在映射唯一时**回答(几个模式挂同一份 preset 时返回
277
+ * `undefined`,不反查)。
278
+ *
279
+ * 本部署的两个模式共享同一个 preset(`MODE_PRESET_ID`),差异全在会话级收口,所以 preset → 模式的反查在
280
+ * 这里无意义:模式由**会话事实**决定(`session-mode/selected` 投影 → 子代理继承 → 部署默认),
281
+ * 官方 roster 选了什么 preset 不改变这个会话是哪个模式。这条反查留给"一对一映射"的部署形态。
282
+ * @param preset - preset id(`undefined` 表示没选过、或 registry 没装)。
283
+ * @returns 该 preset 唯一对应的模式 id;没配扩展、或由多个模式共享时 `undefined`。
284
+ */
285
+ modeForPreset(preset: string | undefined): string | undefined;
110
286
  /** 子代理(有 durable 父会话)继承父当前模式;父不在场、或不是子代理时没有可继承的。 */
111
287
  private inheritedModeId;
112
288
  /** 装或换该 agent 的那一份(幂等:同一模式不重复注册)。 */
113
289
  private installFor;
114
290
  /**
115
- * 模式的默认模型(config 顶层 `models.<模式 id>`)兜底:只在会话**尚无任何模型事实**(没选过模型、也
291
+ * 模式的默认模型(`modes.<模式 id>.defaultModel`)兜底:只在会话**尚无任何模型事实**(没选过模型、也
116
292
  * 还没跑过请求)时接管这一请求的路由;一旦用户选过(投影 `pending`)或会话已经落过 header,就不再插手。
117
293
  *
118
- * 它是**配置事实**,不写会话事件——重启后仍由 config 决定;设置页里那条会话级选择才是会话事实。
119
- *
120
- * 模型取自 `config.models`:那是个 volatile 引用,设置页保存时只有**引用里的值**变,这一行不重挂,
121
- * 所以每次请求都现场 `.get()`(与 `llm-openai-compatible` 读 `config.providers` 同一种读法)。
294
+ * 它是**配置事实**,不写会话事件——重启后仍由 config 决定;设置页里那条会话级选择才是会话事实。读的是构造
295
+ * 时那份模式清单快照:设置页保存会让这一行重挂(`reconcileProfilePatches`),新定义随重挂生效。
122
296
  */
123
297
  private installDefaultModel;
124
298
  /** 收口服务由 `@morlay/dsh-context-assembler/scope` 那一行发布;没装它就只有 persona。 */
@@ -126,4 +300,4 @@ declare class SessionModes extends Service {
126
300
  }
127
301
  declare function apply(ctx: Context, config: ResolvedConfig): void;
128
302
  //#endregion
129
- export { Config, type ResolvedConfig, SESSION_MODE_PATH, type SessionMode, type SessionModeModel, type SessionModeModels, type SessionModePersona, type SessionModeRole, type SessionModeRoster, type SessionModeRow, SessionModes, apply, inject, name, sessionModeProjection };
303
+ export { Config, type ResolvedConfig, SESSION_MODE_PATH, type SessionMode, type SessionModeModel, type SessionModePersona, type SessionModeRole, type SessionModeRoster, type SessionModeRow, SessionModes, apply, inject, name, sessionModeEditableProjection, sessionModeProjection };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,127 @@
1
- import { a as ResolvedConfig, c as SessionModeModels, i as Config, l as SessionModePersona, n as SessionModeRoster, o as SessionMode, r as SessionModeRow, s as SessionModeModel, t as SESSION_MODE_PATH, u as SessionModeRole } from "./shared-FNWe-_rS.mjs";
2
- import { Context, Service } from "@deepseek-ai/cordis";
1
+ import { Context, Service, Volatile } from "@deepseek-ai/cordis";
3
2
  import { z } from "zod";
3
+ import z$1 from "@deepseek-ai/schemastery";
4
4
  import { Agent } from "@deepseek-ai/dsh-agent";
5
5
  import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
6
+ //#region src/modes.d.ts
7
+ /** 一个模式的提示词:两段文本,注册成 agent 作用域的 `deployment:persona-prefix` / `-suffix` section。 */
8
+ interface SessionModePersona {
9
+ /** 系统提示词最前的一段;空串表示不遮蔽部署级那层。 */
10
+ readonly prefix: string;
11
+ /** 系统提示词最后的一段;空串表示不写。 */
12
+ readonly suffix: string;
13
+ }
14
+ /**
15
+ * 一个模式对谁可见:`main` 进用户选择器(会话级选择),`subagent` 表示它**可以**作为子代理的 mode。
16
+ * 两个角色可以同时声明;不写默认 `["main"]`——没写角色的模式不该悄悄变成子代理候选。
17
+ *
18
+ * `subagent` 目前只是候选集的声明:子代理默认继承父 mode(不看角色),"按角色指派 mode" 还没做。
19
+ */
20
+ type SessionModeRole = "main" | "subagent";
21
+ /** 一个模式的默认模型;省略的字段跟着 provider 默认走(与全局 `agent-default-model` 同形状)。 */
22
+ interface SessionModeModel {
23
+ readonly provider: string;
24
+ readonly model: string;
25
+ readonly reasoningEffort?: string;
26
+ }
27
+ /** 退役的顶层形状:模式 id → 模型(默认模型现在住在各自的模式里)。 */
28
+ type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
29
+ /**
30
+ * 一个模式:提示词 + 能力开关。
31
+ *
32
+ * 这份形状是 **schema 归一化之后**的:每个字段都有值(写配置时可以不写,schema 用默认补上——`description`
33
+ * 补空串、`persona` 补两段空文本、两个布尔开关补 `true`)。配置里能省略哪些字段看 `modeSchema` 的 default,
34
+ * 不看这里。
35
+ */
36
+ interface SessionMode {
37
+ /**
38
+ * 这个模式挂在哪个 **agent preset** 上(官方 `standard` / `ptc` / `minimal` / `cordis`,或本部署自己
39
+ * 注册的那一份的 `id`,见 `mode-sources.ts` 的 `MODE_PRESET_ID`)。
40
+ *
41
+ * preset 决定这个 agent 有哪些行(工具 / 命令 / 压缩 / 委派…),模式只决定"这些行怎么被用":
42
+ * `allowTools` 里 preset 没有的工具自动跳过,其余扩展(persona / 注入开关 / 默认模型)照常应用。
43
+ * **可以共享**(本部署就是两个模式挂同一份 preset,差异全在会话级收口);共享时 preset → 模式的反查无从下手,
44
+ * 见 `SessionModes.modeForPreset`。空串表示不挂(只能由 applyTo / 继承使用)。
45
+ */
46
+ readonly preset: string;
47
+ /** 模式的展示名(选择面归官方 roster;这里留着做事实文案)。 */
48
+ readonly name: string;
49
+ /** 一句话说明这个模式干什么;空串表示没写。 */
50
+ readonly description: string;
51
+ /** 这个模式归谁用:`main`(用户选择器)/ `subagent`(可作子代理 mode)。至少一个。 */
52
+ readonly role: SessionModeRole[];
53
+ /** 该模式的提示词。 */
54
+ readonly persona: SessionModePersona;
55
+ /**
56
+ * 这个模式能用哪些工具;其余工具既不进模型目录、调用也被执行层拒绝,它们自己的说明 section
57
+ * (`tool:<工具名>`)也不留在提示词里。**至少给一个**——想要"全都要"就列出全部,别留空。
58
+ */
59
+ readonly allowTools: string[];
60
+ /**
61
+ * 是否要 instruction 类注入(工作区指令、技能目录、用法正文)。缺省要;`false` 表示这个模式一条都不要
62
+ * ——对话模式就是它。开关由 `@morlay/dsh-context-assembler/scope` 落到通道上。
63
+ */
64
+ readonly instructions: boolean;
65
+ /**
66
+ * 是否要 runtime context(文件沙箱策略、审批策略那两条动态快照)。缺省要;`false` 表示这个模式不要它们
67
+ * ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
68
+ */
69
+ readonly runtimeContext: boolean;
70
+ /**
71
+ * 这个模式的默认模型;省略就跟全局 `agent-default-model`。**可选**:没配的模式在页面上不出现在这一行
72
+ * (`defaultModel` 是它所在模式的一个可加字段)。
73
+ */
74
+ readonly defaultModel?: SessionModeModel;
75
+ }
76
+ /** 本包 config 的**源码形状**:装配层与设置页写的那个形状。 */
77
+ interface Config {
78
+ /** 新会话(还没选过模式的会话)用哪个模式。必须是 `modes` 里的一个 id。 */
79
+ readonly default: string;
80
+ /** 模式清单:id → 定义(含各自的 `defaultModel`)。顺序即选择器里的顺序(`Object.entries` 的插入序)。 */
81
+ readonly modes: Record<string, SessionMode>;
82
+ /**
83
+ * 各模式默认模型曾经住在这里(模式 id → 模型)。现在住在**每个模式自己的 `defaultModel`** 里,这个字段
84
+ * 只剩一件事:装配期看见它还配着值就报错,提醒把它挪进对应的模式——否则它会静静地失效。
85
+ */
86
+ readonly models?: SessionModeModels;
87
+ }
88
+ /**
89
+ * schema 解析之后的形状:volatile 字段被换成**稳定引用**,读它要过 `.get()`(设置页改的就是同一份)。
90
+ *
91
+ * `default` 与 `modes` 都是 volatile:默认模式与整份模式清单(含各自的 `defaultModel`)都在行配置页上。
92
+ */
93
+ interface ResolvedConfig {
94
+ readonly default: Volatile<string>;
95
+ readonly modes: Volatile<Record<string, SessionMode>>;
96
+ /** 退役的顶层字段:解析后仍在这儿(普通值,不 volatile),装配期据此发现"还配着值"并报错。 */
97
+ readonly models: SessionModeModels;
98
+ }
99
+ declare const Config: z$1<Config, ResolvedConfig>;
100
+ //#endregion
101
+ //#region src/shared.d.ts
102
+ /**
103
+ * host 半与 client 半共用的接口面:一条 HTTP 路径、模式行的对外形状、请求与响应体。
104
+ *
105
+ * 为什么不是 Typert Remote:客户端的 remote 清单(`@deepseek-ai/dsh-api-remotes/client`)由上游硬编码,
106
+ * 我们的服务不在里面,`ctx.remote.sessionModes` 解析不到。仓库既有的跨半通路是 HTTP 路由
107
+ * (见 `@morlay/ui-conversation-message-actions` 的 `/session-editor`),这里沿用同一种。
108
+ *
109
+ * 会话当前模式不走这条通路:它是一条 session 投影(`sessionMode`),随会话列表一起到页面。
110
+ */
111
+ /** 模式清单与切换的路由路径:宿主(web 与桌面)在同一张路由表上服务它。 */
112
+ declare const SESSION_MODE_PATH = "/session-mode";
113
+ /** 一个模式对外的那部分:选择器要的名字与说明。 */
114
+ interface SessionModeRow {
115
+ readonly id: string;
116
+ readonly name: string;
117
+ readonly description?: string;
118
+ }
119
+ /** `GET` 的响应体:清单与默认模式。 */
120
+ interface SessionModeRoster {
121
+ readonly default: string;
122
+ readonly modes: readonly SessionModeRow[];
123
+ }
124
+ //#endregion
6
125
  //#region src/index.d.ts
7
126
  /** Cordis 插件名:与行 id 一致。 */
8
127
  declare const name = "session-mode";
@@ -33,10 +152,13 @@ declare module "@deepseek-ai/dsh-session/types" {
33
152
  declare module "@deepseek-ai/dsh-session-projection/types" {
34
153
  interface SessionProjectionStateMap {
35
154
  sessionMode: string | null;
155
+ sessionModeEditable: boolean;
36
156
  }
37
157
  interface SessionProjectionMap {
38
158
  /** 会话当前模式;`null` 表示没选过(用部署默认)。 */
39
159
  sessionMode: string | null;
160
+ /** 会话还能不能换模式:`true` 是选择器,`false` 是只读标签(client 那个 chip 据此变形)。 */
161
+ sessionModeEditable: boolean;
40
162
  }
41
163
  }
42
164
  /** 会话模式的投影:初值来自空日志(没选过就是 `null`),只被选择事件推进。 */
@@ -51,11 +173,33 @@ declare const sessionModeProjection: {
51
173
  };
52
174
  stateVersion: number;
53
175
  };
176
+ /**
177
+ * 这个会话能不能换模式的投影:空白会话为 `true`,一旦 `turn/start` 落库就永远 `false`(换模式要的是**整段
178
+ * 历史**的模式一致,所以"开过 turn"之后连正在跑的那个 turn 也算)。
179
+ *
180
+ * **判据只有这一处**:服务端 {@link SessionModes.select} 的拒绝与 client chip 的只读形态都读它——以前
181
+ * client 只能等服务端报错,现在连入口都不给,而两边的结论来自同一份会话事实。
182
+ */
183
+ declare const sessionModeEditableProjection: {
184
+ key: "sessionModeEditable";
185
+ stateSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
186
+ init: () => true;
187
+ apply: (state: boolean, event: SessionEvent) => boolean;
188
+ wire: {
189
+ viewSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
190
+ view: (state: boolean) => boolean;
191
+ };
192
+ stateVersion: number;
193
+ };
54
194
  /** 模式清单、默认模式、按会话读取与切换。 */
55
195
  declare class SessionModes extends Service {
56
- config: ResolvedConfig;
57
196
  /** 每个 agent 已经装上的那一份(persona + 默认模型兜底;模式变了就换一份)。 */
58
197
  private readonly installs;
198
+ /** 装配时的配置快照:`default` / `modes` 读它,改这两项靠 Loader 重挂这一行(已运行会话不自动换定义)。 */
199
+ readonly config: {
200
+ default: string;
201
+ modes: Record<string, SessionMode>;
202
+ };
59
203
  constructor(ctx: Context, config: ResolvedConfig);
60
204
  /** 新会话用它:config 里的 `default`。 */
61
205
  get defaultId(): string;
@@ -86,11 +230,30 @@ declare class SessionModes extends Service {
86
230
  modeOfSession(session: Session): SessionMode;
87
231
  /**
88
232
  * 把某个空白会话切到某个模式。
233
+ *
234
+ * 模式带着它的 preset(`preset` 决定行清单),所以这里**先把 agent preset 换成模式声明的那个**,再落我们的
235
+ * 会话事实:否则 preset realm 还是旧那一套的行,而它的注入在我们的开关之外(旧形态 `chat` 挂 `minimal`、
236
+ * 会话的 preset 却还是 `standard` 时,上游 `agent-instructions` 会照旧把工作区指令注进这个"不要注入"的会话)。
237
+ *
238
+ * 目标 preset 与当前挂着的**相同时不切**(本部署两个模式共享同一份 preset,所以切模式通常走不到这一步):
239
+ * 换 preset 是一次重挂(卸旧行、装新行),没有变化就没有理由付出这个代价。
89
240
  * @param sessionId - 目标会话(必须还没有开过 turn)。
90
241
  * @param mode - 目标模式 id。
91
242
  * @returns 提交后的模式 id。
92
243
  */
93
244
  select(sessionId: SessionId, mode: string): Promise<string>;
245
+ /**
246
+ * 某个 agent 当前挂着的 preset(registry 的 `composedPreset`)。
247
+ *
248
+ * 读不到时返回 `undefined`(按"未知"处理 → 该切就切):`composedPreset` 是 registry 较新的读面,替身与老
249
+ * 版本可能没有它;而"没挂任何 preset"(返回值 `undefined`)与"读不到"在这里是同一个结论。
250
+ */
251
+ private presetOfAgent;
252
+ /**
253
+ * 官方 preset registry:行清单与它的选择面住在那一行。不在 `inject` 里点名(headless 部署没有它,
254
+ * 点名会让本行永不激活),所以按"可能拿不到"读——`ctx.get` 在当前 ctx 没声明那个服务时会抛。
255
+ */
256
+ private presetRegistry;
94
257
  /**
95
258
  * 把某个活着的 agent 切到某个模式。与 {@link select} 的差别:它不要求空白会话——子代理创建时的继承
96
259
  * 走这里,**未来的"指定 mode"入口(模型侧或配置侧)也走这里**(那条接缝还没做)。
@@ -107,18 +270,29 @@ declare class SessionModes extends Service {
107
270
  * 继承要**写进子会话日志**:它是一条会话事实,冷恢复与 fork 都要靠它重建({@link modeOf} 只读投影)。
108
271
  */
109
272
  private resolveModeId;
273
+ /** 官方 roster 选定的 preset(没选过、或 registry 没装时为 `undefined`)。 */
274
+ private presetOf;
275
+ /**
276
+ * 某个 agent preset 对应的模式 id——**只在映射唯一时**回答(几个模式挂同一份 preset 时返回
277
+ * `undefined`,不反查)。
278
+ *
279
+ * 本部署的两个模式共享同一个 preset(`MODE_PRESET_ID`),差异全在会话级收口,所以 preset → 模式的反查在
280
+ * 这里无意义:模式由**会话事实**决定(`session-mode/selected` 投影 → 子代理继承 → 部署默认),
281
+ * 官方 roster 选了什么 preset 不改变这个会话是哪个模式。这条反查留给"一对一映射"的部署形态。
282
+ * @param preset - preset id(`undefined` 表示没选过、或 registry 没装)。
283
+ * @returns 该 preset 唯一对应的模式 id;没配扩展、或由多个模式共享时 `undefined`。
284
+ */
285
+ modeForPreset(preset: string | undefined): string | undefined;
110
286
  /** 子代理(有 durable 父会话)继承父当前模式;父不在场、或不是子代理时没有可继承的。 */
111
287
  private inheritedModeId;
112
288
  /** 装或换该 agent 的那一份(幂等:同一模式不重复注册)。 */
113
289
  private installFor;
114
290
  /**
115
- * 模式的默认模型(config 顶层 `models.<模式 id>`)兜底:只在会话**尚无任何模型事实**(没选过模型、也
291
+ * 模式的默认模型(`modes.<模式 id>.defaultModel`)兜底:只在会话**尚无任何模型事实**(没选过模型、也
116
292
  * 还没跑过请求)时接管这一请求的路由;一旦用户选过(投影 `pending`)或会话已经落过 header,就不再插手。
117
293
  *
118
- * 它是**配置事实**,不写会话事件——重启后仍由 config 决定;设置页里那条会话级选择才是会话事实。
119
- *
120
- * 模型取自 `config.models`:那是个 volatile 引用,设置页保存时只有**引用里的值**变,这一行不重挂,
121
- * 所以每次请求都现场 `.get()`(与 `llm-openai-compatible` 读 `config.providers` 同一种读法)。
294
+ * 它是**配置事实**,不写会话事件——重启后仍由 config 决定;设置页里那条会话级选择才是会话事实。读的是构造
295
+ * 时那份模式清单快照:设置页保存会让这一行重挂(`reconcileProfilePatches`),新定义随重挂生效。
122
296
  */
123
297
  private installDefaultModel;
124
298
  /** 收口服务由 `@morlay/dsh-context-assembler/scope` 那一行发布;没装它就只有 persona。 */
@@ -126,4 +300,4 @@ declare class SessionModes extends Service {
126
300
  }
127
301
  declare function apply(ctx: Context, config: ResolvedConfig): void;
128
302
  //#endregion
129
- export { Config, type ResolvedConfig, SESSION_MODE_PATH, type SessionMode, type SessionModeModel, type SessionModeModels, type SessionModePersona, type SessionModeRole, type SessionModeRoster, type SessionModeRow, SessionModes, apply, inject, name, sessionModeProjection };
303
+ export { Config, type ResolvedConfig, SESSION_MODE_PATH, type SessionMode, type SessionModeModel, type SessionModePersona, type SessionModeRole, type SessionModeRoster, type SessionModeRow, SessionModes, apply, inject, name, sessionModeEditableProjection, sessionModeProjection };