@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/src/modes.ts
CHANGED
|
@@ -1,105 +1,86 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* 与旧形态(每个模式一行 `@deepseek-ai/dsh-agent-preset`,`config.plugins` 里塞 persona / scope 行)的区别:
|
|
5
|
-
* 模式不再是 Cordis 子树,而是这份纯数据——本包的插件行只在 `config.modes` 里声明它们,运行期按会话读取
|
|
6
|
-
* 并应用(persona 注册到该 agent 的 scope,工具收口交给 `@morlay/dsh-context-assembler/scope`)。
|
|
7
|
-
*
|
|
8
|
-
* 行清单(工具 / 命令 / 压缩 / 委派…)来自 `preset` 指的那份 agent preset:本部署自己注册了一份
|
|
9
|
-
* (`packages/bundles/session-mode-profile` 的 `preset-mode-switch`),两个模式共享它,差异全在会话级收口。
|
|
10
|
-
*
|
|
11
|
-
* "支持自定义"就是指这份 config:装配层(`cordis.patch.yml` / profile 的用户层)能整体改写 `modes`,
|
|
12
|
-
* 也可以只给某几个模式换提示词或白名单——不需要任何插件行。
|
|
13
|
-
*
|
|
14
|
-
* "某个模式默认用哪个模型"**不在**模式里,而是 config 的顶层 `models`(模式 id → 模型):它是 settings
|
|
15
|
-
* 的设置面要编辑的东西,而设置面只认 volatile 字段、且只认**固定路径**(dict 内部的字段一律 blocked,见
|
|
16
|
-
* `@deepseek-ai/schemastery` 的 `validateVolatileSchema`)。取舍与理由见
|
|
17
|
-
* [ADR 模式默认模型搬到顶层 volatile](../.agents/adrs/20260925-模式默认模型搬到顶层volatile.md)。
|
|
18
|
-
*/
|
|
1
|
+
// 模式的定义形状与它的装配期校验:一个模式就是"一段提示词 + 一组能力开关",本行的 `config.modes` 声明它们。
|
|
2
|
+
// 行清单来自 `preset` 指的那份 agent preset;装配层能整体改写 `modes`,也可以只给某几个模式换提示词或白名单。
|
|
3
|
+
// 取舍见 `.agents/adrs/20260925-默认模型住在模式定义里.md`、`.agents/designs/20260924-会话模式.md`。
|
|
19
4
|
|
|
20
5
|
import type { Volatile } from "@deepseek-ai/cordis";
|
|
21
6
|
import z from "@deepseek-ai/schemastery";
|
|
7
|
+
import { POLICY_NAMES } from "./shared.ts";
|
|
22
8
|
|
|
23
|
-
|
|
9
|
+
// 一个模式的提示词:两段文本,注册成 agent 作用域的 `deployment:persona-prefix` / `-suffix` section。
|
|
24
10
|
export interface SessionModePersona {
|
|
25
|
-
|
|
11
|
+
// 系统提示词最前的一段;空串表示不遮蔽部署级那层。
|
|
26
12
|
readonly prefix: string;
|
|
27
|
-
|
|
13
|
+
// 系统提示词最后的一段;空串表示不写。
|
|
28
14
|
readonly suffix: string;
|
|
29
15
|
}
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* 两个角色可以同时声明;不写默认 `["main"]`——没写角色的模式不该悄悄变成子代理候选。
|
|
34
|
-
*
|
|
35
|
-
* `subagent` 目前只是候选集的声明:子代理默认继承父 mode(不看角色),"按角色指派 mode" 还没做。
|
|
36
|
-
*/
|
|
17
|
+
// 一个模式对谁可见:`main` 进用户选择器(会话级选择),`subagent` 表示它**可以**作为子代理的 mode。
|
|
18
|
+
// 两个角色可以同时声明;不写默认 `["main"]`。`subagent` 现在只是候选集的声明——子代理默认继承父 mode。
|
|
37
19
|
export type SessionModeRole = "main" | "subagent";
|
|
38
20
|
|
|
39
|
-
|
|
21
|
+
// 一个模式的默认模型;省略的字段跟着 provider 默认走(与全局 `agent-default-model` 同形状)。
|
|
40
22
|
export interface SessionModeModel {
|
|
41
23
|
readonly provider: string;
|
|
42
24
|
readonly model: string;
|
|
43
25
|
readonly reasoningEffort?: string;
|
|
44
26
|
}
|
|
45
27
|
|
|
46
|
-
|
|
28
|
+
// 退役的顶层形状:模式 id → 模型(默认模型现在住在各自的模式里)。
|
|
47
29
|
export type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*
|
|
52
|
-
* 这份形状是 **schema 归一化之后**的:每个字段都有值(写配置时可以不写,schema 用默认补上——`description`
|
|
53
|
-
* 补空串、`persona` 补两段空文本、两个布尔开关补 `true`)。配置里能省略哪些字段看 `modeSchema` 的 default,
|
|
54
|
-
* 不看这里。
|
|
55
|
-
*/
|
|
31
|
+
// 一个模式:提示词 + 能力开关。这份形状是 **schema 归一化之后**的(每个字段都有值,schema 用默认补上),
|
|
32
|
+
// 配置里能省略哪些字段看 `modeSchema` 的 default。
|
|
56
33
|
export interface SessionMode {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* preset 决定这个 agent 有哪些行(工具 / 命令 / 压缩 / 委派…),模式只决定"这些行怎么被用":
|
|
62
|
-
* `allowTools` 里 preset 没有的工具自动跳过,其余扩展(persona / 注入开关 / 默认模型)照常应用。
|
|
63
|
-
* **可以共享**(本部署就是两个模式挂同一份 preset,差异全在会话级收口);共享时 preset → 模式的反查无从下手,
|
|
64
|
-
* 见 `SessionModes.modeForPreset`。空串表示不挂(只能由 applyTo / 继承使用)。
|
|
65
|
-
*/
|
|
34
|
+
// 这个模式挂在哪个 **agent preset** 上(官方四个,或本部署自己注册的那一份)。**可选**:不写(schema 里是空串)
|
|
35
|
+
// 就是不绑——选这个模式不换 preset,会话保持它当前挂着的那份,行清单由那份 preset 提供;模式自己的那几项扩展
|
|
36
|
+
// (persona / 工具名单 / policy 名单 / 三个开关 / `defaultModel`)在任意 preset 上都照常生效。
|
|
37
|
+
// 写了才在切模式时切过去;**可以共享**(共享时反查无从下手,见 `SessionModes.modeForPreset`)。
|
|
66
38
|
readonly preset: string;
|
|
67
|
-
|
|
39
|
+
// 模式的展示名(选择面归官方 roster;这里留着做事实文案)。
|
|
68
40
|
readonly name: string;
|
|
69
|
-
|
|
41
|
+
// 一句话说明这个模式干什么;空串表示没写。
|
|
70
42
|
readonly description: string;
|
|
71
|
-
|
|
43
|
+
// 这个模式归谁用:`main`(用户选择器)/ `subagent`(可作子代理 mode)。至少一个。
|
|
72
44
|
readonly role: SessionModeRole[];
|
|
73
|
-
|
|
45
|
+
// 该模式的提示词。
|
|
74
46
|
readonly persona: SessionModePersona;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
*/
|
|
47
|
+
// 这个模式能用哪些工具;其余工具既不进模型目录、调用也被执行层拒绝,它们自己的说明 section
|
|
48
|
+
// (`tool:<工具名>`)也不留在提示词里。**留空表示不设收窄**:这个会话用它挂着的 preset 装着的全部工具
|
|
49
|
+
// (想收窄就列名单——"全都要"不需要抄一份清单)。
|
|
79
50
|
readonly allowTools: string[];
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
51
|
+
// 这个模式**不用**哪些工具(黑名单):从 `allowTools` 定下的那份里减掉(`allowTools` 留空时就是从全部里减)。
|
|
52
|
+
// 留空表示不禁任何工具。同时命中两份名单时以这里为准(deny 优先)。
|
|
53
|
+
readonly denyTools: string[];
|
|
54
|
+
// 上游 policy 规则的**生效白名单**:留空(或不写)表示全部规则照旧生效;有值表示只有列出的那些生效,
|
|
55
|
+
// 其余的按 `denyPolicies` 那一套被绕过。名单见 `POLICY_NAMES`。空数组与不写同义。
|
|
56
|
+
readonly allowPolicies: string[];
|
|
57
|
+
// 上游 policy 规则的黑名单:列出的规则**禁用**——上游在那条 waterfall 上的裁决(连它抛出的拒绝)被丢掉,
|
|
58
|
+
// 调用按"没有这条规则"继续。留空表示一条都不禁;同时命中两份名单时以这里为准(deny 优先)。
|
|
59
|
+
readonly denyPolicies: string[];
|
|
60
|
+
// 是否要 instruction 类注入。缺省要;`false` 表示这个模式一条都不要——对话模式就是它。落到两处:丢掉官方
|
|
61
|
+
// `agent-instructions` 的注入(`agent/pre-step` 上过滤),以及关掉通道自己的降级注入。
|
|
84
62
|
readonly instructions: boolean;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
63
|
+
// 是否要技能目录。**可选**:不写就由这个模式自己的工具名单推导——`(allowTools 留空 ? 全部 : allowTools) −
|
|
64
|
+
// denyTools` 里含 `skill` 就要(`allowTools` 留空即"全部",所以只有 `denyTools` 能把它推成 `false`)。
|
|
65
|
+
// `false` 表示这个模式不要技能目录:丢掉官方 `skill-catalog` 的注入(`skill` 工具的收窄仍归 `allowTools`)。
|
|
66
|
+
readonly skills?: boolean;
|
|
67
|
+
// 是否要 runtime context(文件沙箱策略、审批策略那两条动态快照)。缺省要;`false` 表示这个模式不要它们
|
|
68
|
+
// ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
|
|
89
69
|
readonly runtimeContext: boolean;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* (`defaultModel` 是它所在模式的一个可加字段)。
|
|
93
|
-
*/
|
|
70
|
+
// 这个模式的默认模型;省略就跟全局 `agent-default-model`。**可选**:没配的模式在页面上不出现在这一行
|
|
71
|
+
// (`defaultModel` 是它所在模式的一个可加字段)。
|
|
94
72
|
readonly defaultModel?: SessionModeModel;
|
|
95
73
|
}
|
|
96
74
|
|
|
75
|
+
// `skills` 不写时的推导:起点是白名单(留空 = 起点是全部工具,含 `skill`),减去黑名单,还留着 `skill` 就要技能
|
|
76
|
+
// 目录。名单留空等于"全部工具",所以只有 `denyTools` 能把它推成 `false`。
|
|
77
|
+
export function derivedSkills(mode: Pick<SessionMode, "allowTools" | "denyTools">): boolean {
|
|
78
|
+
const allowed = mode.allowTools.length === 0 || mode.allowTools.includes("skill");
|
|
79
|
+
return allowed && !mode.denyTools.includes("skill");
|
|
80
|
+
}
|
|
81
|
+
|
|
97
82
|
// 每个字段都带 default:schema 的产物因此没有 `undefined` 键(`exactOptionalPropertyTypes` 下"缺省的键"
|
|
98
|
-
//
|
|
99
|
-
/**
|
|
100
|
-
* 本地化说明:`description()` 的类型签名只声明 `string`,而 meta 本身接受 `Dict<string>`
|
|
101
|
-
* (`vendor/schemastery/src/index.ts` 的 `mergeDesc` 就是按字典合并的),所以这里只做一次类型放行。
|
|
102
|
-
*/
|
|
83
|
+
// 会与可选属性对不上)。`description()` 的类型签名只声明 `string`,这里做一次类型放行。
|
|
103
84
|
const localized = (text: { zh: string; en: string }): string => text as unknown as string;
|
|
104
85
|
|
|
105
86
|
const personaSchema = z.object({
|
|
@@ -107,10 +88,10 @@ const personaSchema = z.object({
|
|
|
107
88
|
suffix: z.string().default(""),
|
|
108
89
|
});
|
|
109
90
|
|
|
110
|
-
|
|
91
|
+
// 角色是个封闭集合:写错的 role 在装配期就拒绝,而不是静默变成"谁都不用"。
|
|
111
92
|
const roleSchema = z.union([z.const("main"), z.const("subagent")]);
|
|
112
93
|
|
|
113
|
-
|
|
94
|
+
// 默认模型的形状与全局 `agent-default-model` 一致;省略 effort 就跟 provider 默认。
|
|
114
95
|
const modelSchema = z.object({
|
|
115
96
|
provider: z
|
|
116
97
|
.string()
|
|
@@ -140,28 +121,24 @@ const modelSchema = z.object({
|
|
|
140
121
|
),
|
|
141
122
|
});
|
|
142
123
|
|
|
143
|
-
|
|
124
|
+
// 本包 config 的**源码形状**:装配层与设置页写的那个形状。
|
|
144
125
|
export interface Config {
|
|
145
|
-
|
|
126
|
+
// 新会话(还没选过模式的会话)用哪个模式。必须是 `modes` 里的一个 id。
|
|
146
127
|
readonly default: string;
|
|
147
|
-
|
|
128
|
+
// 模式清单:id → 定义(含各自的 `defaultModel`)。顺序即选择器里的顺序(`Object.entries` 的插入序)。
|
|
148
129
|
readonly modes: Record<string, SessionMode>;
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
* 只剩一件事:装配期看见它还配着值就报错,提醒把它挪进对应的模式——否则它会静静地失效。
|
|
152
|
-
*/
|
|
130
|
+
// 退役字段:各模式的默认模型住在每个模式自己的 `defaultModel` 里;这里留着只为装配期报错
|
|
131
|
+
// (见 `configProblem`)。
|
|
153
132
|
readonly models?: SessionModeModels;
|
|
154
133
|
}
|
|
155
134
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
* `default` 与 `modes` 都是 volatile:默认模式与整份模式清单(含各自的 `defaultModel`)都在行配置页上。
|
|
160
|
-
*/
|
|
135
|
+
// schema 解析之后的形状:volatile 字段被换成**稳定引用**,读它要过 `.get()`(设置页改的就是同一份)。
|
|
136
|
+
//
|
|
137
|
+
// `default` 与 `modes` 都是 volatile:默认模式与整份模式清单(含各自的 `defaultModel`)都在行配置页上。
|
|
161
138
|
export interface ResolvedConfig {
|
|
162
139
|
readonly default: Volatile<string>;
|
|
163
140
|
readonly modes: Volatile<Record<string, SessionMode>>;
|
|
164
|
-
|
|
141
|
+
// 退役的顶层字段:解析后仍在这儿(普通值,不 volatile),装配期据此发现"还配着值"并报错。
|
|
165
142
|
readonly models: SessionModeModels;
|
|
166
143
|
}
|
|
167
144
|
|
|
@@ -171,8 +148,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
171
148
|
.default("")
|
|
172
149
|
.description(
|
|
173
150
|
localized({
|
|
174
|
-
zh: "这个模式挂哪个 agent preset(它的 id
|
|
175
|
-
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
|
|
151
|
+
zh: "这个模式挂哪个 agent preset(它的 id,官方或本部署自建的):行清单由那份 preset 提供,几个模式可以共享同一个。留空就是不绑——选这个模式不换 preset,会话保持当前挂着的那份,模式自己的提示词、工具收口与开关照常生效。",
|
|
152
|
+
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.",
|
|
176
153
|
}),
|
|
177
154
|
),
|
|
178
155
|
name: z
|
|
@@ -208,8 +185,35 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
208
185
|
.default([])
|
|
209
186
|
.description(
|
|
210
187
|
localized({
|
|
211
|
-
zh: "
|
|
212
|
-
en: "Tools this session may use; everything else leaves the catalog and calls are refused.",
|
|
188
|
+
zh: "这个会话能用的工具;其余既不进目录,调用也被拒。**留空就是不设收窄**:用这个会话挂着的 preset 提供的全部工具。",
|
|
189
|
+
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.",
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
denyTools: z
|
|
193
|
+
.array(z.string())
|
|
194
|
+
.default([])
|
|
195
|
+
.description(
|
|
196
|
+
localized({
|
|
197
|
+
zh: "这个模式不用的工具(黑名单):从 `allowTools` 定下的那份里减掉(`allowTools` 留空就是从全部里减)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
198
|
+
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.",
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
201
|
+
allowPolicies: z
|
|
202
|
+
.array(z.string())
|
|
203
|
+
.default([])
|
|
204
|
+
.description(
|
|
205
|
+
localized({
|
|
206
|
+
zh: "上游 policy 规则的生效白名单(`fs/write-intent` / `fs/edit-intent`):留空 = 全部规则照旧生效;有值 = 只有列出的生效,其余的被绕过。",
|
|
207
|
+
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.",
|
|
208
|
+
}),
|
|
209
|
+
),
|
|
210
|
+
denyPolicies: z
|
|
211
|
+
.array(z.string())
|
|
212
|
+
.default([])
|
|
213
|
+
.description(
|
|
214
|
+
localized({
|
|
215
|
+
zh: "上游 policy 规则的黑名单:列出的规则禁用(它的裁决连拒绝一起丢掉,调用按没有这条规则继续)。留空 = 一条都不禁;同时命中两份名单时以这里为准。",
|
|
216
|
+
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.",
|
|
213
217
|
}),
|
|
214
218
|
),
|
|
215
219
|
instructions: z
|
|
@@ -217,10 +221,16 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
217
221
|
.default(true)
|
|
218
222
|
.description(
|
|
219
223
|
localized({
|
|
220
|
-
zh: "是否要 instruction
|
|
221
|
-
en: "Whether instruction-class injections apply (workspace instructions,
|
|
224
|
+
zh: "是否要 instruction 类注入(工作区指令、用法正文):`false` 时丢掉官方 `agent-instructions` 的注入,并关掉通道自己的降级注入。",
|
|
225
|
+
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.",
|
|
222
226
|
}),
|
|
223
227
|
),
|
|
228
|
+
skills: z.boolean().description(
|
|
229
|
+
localized({
|
|
230
|
+
zh: "是否要技能目录(官方 `skill-catalog` 的注入)。**不写就按这个模式自己的工具名单推导**:`(allowTools 留空 ? 全部 : allowTools) − denyTools` 里含 `skill` 就要;写 `false` 就丢掉官方 `skill-catalog` 的注入(`skill` 工具的可见性仍归 `allowTools`)。",
|
|
231
|
+
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`).",
|
|
232
|
+
}),
|
|
233
|
+
),
|
|
224
234
|
runtimeContext: z
|
|
225
235
|
.boolean()
|
|
226
236
|
.default(true)
|
|
@@ -230,10 +240,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
230
240
|
en: "Whether the runtime snapshot applies (sandbox and approval policy).",
|
|
231
241
|
}),
|
|
232
242
|
),
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
* schemastery 拒(`validateVolatileSchema` 不许 volatile 套 volatile)。
|
|
236
|
-
*/
|
|
243
|
+
// 这个模式的默认模型。不标 `volatile`:`modes` 本身就是 volatile,整棵子树都在页面上——再标一层会被
|
|
244
|
+
// schemastery 拒(`validateVolatileSchema` 不许 volatile 套 volatile)。
|
|
237
245
|
defaultModel: modelSchema
|
|
238
246
|
// `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
|
|
239
247
|
// 凭空多出一行;给了 null 反而让它保持缺失(页面按非必填处理,从候选加成)。
|
|
@@ -264,43 +272,51 @@ export const Config: z<Config, ResolvedConfig> = z.object({
|
|
|
264
272
|
.description(
|
|
265
273
|
localized({
|
|
266
274
|
zh:
|
|
267
|
-
"模式清单:id → 定义(persona /
|
|
268
|
-
"
|
|
275
|
+
"模式清单:id → 定义(persona / 工具名单 / 生效的 policy 规则 / 角色)。改它对**已运行会话**不自动生效" +
|
|
276
|
+
"——重挂后新建的会话、或重新应用模式的会话才用新定义。",
|
|
269
277
|
en:
|
|
270
|
-
"Mode roster: id to definition (persona,
|
|
271
|
-
"sessions; sessions created after the row is remounted use the new definition.",
|
|
278
|
+
"Mode roster: id to definition (persona, tool lists, effective policy rules, role). Edits do not follow " +
|
|
279
|
+
"into already-running sessions; sessions created after the row is remounted use the new definition.",
|
|
272
280
|
}),
|
|
273
281
|
)
|
|
274
282
|
.volatile(),
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
* 页面上不出现(`hidden()`)。
|
|
278
|
-
*/
|
|
283
|
+
// 退役字段:默认模型住在每个模式自己的 `defaultModel` 里。这里留着是为了**报错**(见 `configProblem`),
|
|
284
|
+
// 页面上不出现(`hidden()`)。
|
|
279
285
|
models: z.dict(modelSchema).default({}).hidden(),
|
|
280
286
|
});
|
|
281
287
|
|
|
282
|
-
|
|
288
|
+
// 校验只需要看的那几件事:默认模式、每个模式的角色、工具名单、policy 名单与默认模型。
|
|
283
289
|
interface Validated {
|
|
284
290
|
readonly default: string;
|
|
285
291
|
readonly modes: Readonly<
|
|
286
292
|
Record<
|
|
287
293
|
string,
|
|
288
294
|
{
|
|
289
|
-
|
|
295
|
+
// 空串合法的"不挂";共享合法(差异由会话级收口表达),所以这里不做任何映射唯一性校验。
|
|
290
296
|
readonly preset?: string;
|
|
297
|
+
// 留空合法:不设收窄(用 preset 的全部工具)。
|
|
291
298
|
readonly allowTools?: readonly string[];
|
|
299
|
+
// 留空合法:不禁任何工具;与 `allowTools` 同时命中合法(deny 优先)。
|
|
300
|
+
readonly denyTools?: readonly string[];
|
|
301
|
+
// 留空合法:全部规则生效。
|
|
302
|
+
readonly allowPolicies?: readonly string[];
|
|
303
|
+
// 留空合法:一条都不禁;与 `allowPolicies` 同时命中合法(deny 优先)。
|
|
304
|
+
readonly denyPolicies?: readonly string[];
|
|
292
305
|
readonly role?: readonly string[];
|
|
293
306
|
readonly defaultModel?: { readonly provider?: string; readonly model?: string };
|
|
294
307
|
}
|
|
295
308
|
>
|
|
296
309
|
>;
|
|
297
|
-
|
|
310
|
+
// 退役的顶层字段:还配着值就报错(它已经不再生效)。
|
|
298
311
|
readonly models?: Readonly<Record<string, unknown>>;
|
|
299
312
|
}
|
|
300
313
|
|
|
301
|
-
|
|
314
|
+
// 已知的 policy 名(上游 waterfall 名):写错的名字静默变成"没配"是这份配置最坏的失效方式,所以装配期拒绝。
|
|
315
|
+
const POLICY_NAME_SET: ReadonlySet<string> = new Set(POLICY_NAMES);
|
|
316
|
+
|
|
317
|
+
// 模式定义里不合法的地方(装配期 fail loud,而不是等到某个会话装配提示词时才发现)。
|
|
302
318
|
export function configProblem(config: Validated): string | undefined {
|
|
303
|
-
|
|
319
|
+
// 没写 `role` 等于默认 `["main"]`(与 schema 的默认一致)——字面量与归一化后的形状都能校验。
|
|
304
320
|
const roles = (id: string): readonly string[] => config.modes[id]?.role ?? ["main"];
|
|
305
321
|
const ids = Object.keys(config.modes);
|
|
306
322
|
if (ids.length === 0) return "session-mode: `modes` must declare at least one mode";
|
|
@@ -314,14 +330,19 @@ export function configProblem(config: Validated): string | undefined {
|
|
|
314
330
|
if (!roles(config.default).includes("main")) {
|
|
315
331
|
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`;
|
|
316
332
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
333
|
+
// 每个模式列出的 policy 名都必须在已知名单里(名单见 `shared.ts` 的 `POLICY_NAMES`)。`denyTools` 与
|
|
334
|
+
// `allowTools` 同时命中是**合法**的(deny 优先):这里不报错,只拒绝认不出的名字。
|
|
335
|
+
const unknownPolicies = Object.entries(config.modes).flatMap(([id, mode]) =>
|
|
336
|
+
[...(mode.allowPolicies ?? []), ...(mode.denyPolicies ?? [])]
|
|
337
|
+
.filter((policy) => !POLICY_NAME_SET.has(policy))
|
|
338
|
+
.map((policy) => `${id}: ${policy}`),
|
|
339
|
+
);
|
|
340
|
+
if (unknownPolicies.length > 0) {
|
|
341
|
+
return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${POLICY_NAMES.join(", ")}`;
|
|
320
342
|
}
|
|
321
|
-
// `
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
// 退役的顶层字段还配着值:它已经不再生效,别让一份"看着像配过"的配置静静地失效。
|
|
343
|
+
// `allowTools` **留空是合法的**:不设收窄,用这个会话挂着的 preset 的全部工具。
|
|
344
|
+
// `preset` **允许共享**(共享时 preset → 模式的反查交给 `SessionModes.modeForPreset`),空串是"不挂";
|
|
345
|
+
// 退役的顶层 `models` 还配着值就报错——别让一份"看着像配过"的配置静静地失效。
|
|
325
346
|
if (Object.keys(config.models ?? {}).length > 0) {
|
|
326
347
|
return "session-mode: `models` has moved into each mode's `defaultModel`; move the entries there and drop the top-level `models`";
|
|
327
348
|
}
|
package/src/persona.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* 为什么不是一行 `@deepseek-ai/dsh-persona`:那一行只能按 scope 遮蔽,得先有一棵 preset 子树。这里走的是
|
|
5
|
-
* 上游自己给子 agent 用的那条路——在 `agent.ctx` 上注册同名 section(`deployment:persona-prefix` /
|
|
6
|
-
* `-suffix`),跨层遮蔽部署级那层,不需要任何子树。
|
|
7
|
-
*
|
|
8
|
-
* 两个 section 都注册(哪怕只给了一段文本):suffix 缺省就是空串,语义与上游 persona 行一致。注册与
|
|
9
|
-
* 注销都发 `system-prompt/change`,所以模式切换后下一次装配自然读到新文本。
|
|
10
|
-
*/
|
|
1
|
+
// 按会话给提示词:把模式的 persona 注册到该 agent 自己的 scope 上(`deployment:persona-prefix` / `-suffix`),
|
|
2
|
+
// 跨层遮蔽部署级那层。两个 section 都注册(缺省是空串);注册与注销都发 `system-prompt/change`。
|
|
11
3
|
|
|
12
4
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
13
5
|
import type { PromptSectionOrderName } from "@deepseek-ai/dsh-system-prompt";
|
|
@@ -18,12 +10,7 @@ import type { SessionModePersona } from "./modes.ts";
|
|
|
18
10
|
const PREFIX_ORDER: PromptSectionOrderName = "DEPLOYMENT_PERSONA_PREFIX";
|
|
19
11
|
const SUFFIX_ORDER: PromptSectionOrderName = "DEPLOYMENT_PERSONA_SUFFIX";
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
* 把一段 persona 装到该 agent 的 scope 上。
|
|
23
|
-
* @param agent - 目标 agent(用它的 `ctx` 定作用域)。
|
|
24
|
-
* @param persona - 模式的提示词;缺省时两段都注册成空串,等于遮蔽掉部署级那层。
|
|
25
|
-
* @returns 注销这两个 section 的 disposer(模式切换时先调它)。
|
|
26
|
-
*/
|
|
13
|
+
// 把一段 persona 装到该 agent 的 scope 上;返回注销这两个 section 的 disposer(模式切换时先调它)。
|
|
27
14
|
export function installPersona(agent: Agent, persona: SessionModePersona | undefined): () => void {
|
|
28
15
|
const prompt = agent.ctx.systemPrompt;
|
|
29
16
|
const disposers = [
|
package/src/rows.ts
CHANGED
|
@@ -1,42 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* 装配入口在 `packages/bundles/session-mode-profile`(它同时装注入通道、工具说明与 subagent 那几行)。
|
|
4
|
-
*/
|
|
1
|
+
// 本包作为能力包发布的**装配数据**:`session-mode` 行的 config(各模式的会话级扩展)由这里渲染,
|
|
2
|
+
// 装配入口在 `packages/bundles/session-mode-profile`(它同时装注入通道、工具说明与 subagent 那几行)。
|
|
5
3
|
|
|
6
4
|
import { DEFAULT_MODE, MODE_SOURCES, type ModeSource } from "./mode-sources.ts";
|
|
7
5
|
|
|
8
|
-
//
|
|
9
|
-
// 是同一个事实,所以只有一份。
|
|
10
|
-
export { MODE_PRESET_ID } from "./mode-sources.ts";
|
|
11
|
-
|
|
12
|
-
/** 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。 */
|
|
6
|
+
// 一行装配条目:与 `cordis.patch.yml` 的顶层结构同形。
|
|
13
7
|
export interface PatchRow {
|
|
14
8
|
readonly insert?: readonly RowEntry[];
|
|
15
9
|
}
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
// `insert` 里的一个条目。
|
|
18
12
|
export interface RowEntry {
|
|
19
13
|
readonly id: string;
|
|
20
14
|
readonly name: string;
|
|
21
15
|
readonly config?: Readonly<Record<string, unknown>>;
|
|
22
16
|
}
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
// 一份名单:留空(或没写)就不进 config——空数组与不写同义,行 config 里不留空键。
|
|
19
|
+
function list<T>(values: readonly T[] | undefined): readonly T[] | undefined {
|
|
20
|
+
return values === undefined || values.length === 0 ? undefined : [...values];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 一个模式的源定义 → 行 config(schema 的输入形状:可省的字段就省)。
|
|
25
24
|
function modeConfig(source: ModeSource): Record<string, unknown> {
|
|
25
|
+
const allowTools = list(source.allowTools);
|
|
26
|
+
const denyTools = list(source.denyTools);
|
|
27
|
+
const allowPolicies = list(source.allowPolicies);
|
|
28
|
+
const denyPolicies = list(source.denyPolicies);
|
|
26
29
|
return {
|
|
27
|
-
preset
|
|
30
|
+
// `preset` 是可选的:不写就是不绑(行 config 里也不出现这个键,schema 默认空串)。
|
|
31
|
+
...(source.preset === undefined ? {} : { preset: source.preset }),
|
|
28
32
|
name: source.name,
|
|
29
33
|
description: source.description,
|
|
30
34
|
...(source.role === undefined ? {} : { role: [...source.role] }),
|
|
31
35
|
...(source.persona === undefined ? {} : { persona: { ...source.persona } }),
|
|
32
|
-
|
|
36
|
+
// 四份名单都是可选的:不写(或写成空数组)就是这个键不进 config,schema 默认空数组。
|
|
37
|
+
// `allowTools` 不写 = 不设收窄;`denyTools` 不写 = 一件都不禁(两份同配时 deny 优先);
|
|
38
|
+
// `allowPolicies` 不写 = 全部上游 policy 规则生效;`denyPolicies` 不写 = 一条都不禁。
|
|
39
|
+
...(allowTools === undefined ? {} : { allowTools }),
|
|
40
|
+
...(denyTools === undefined ? {} : { denyTools }),
|
|
41
|
+
...(allowPolicies === undefined ? {} : { allowPolicies }),
|
|
42
|
+
...(denyPolicies === undefined ? {} : { denyPolicies }),
|
|
33
43
|
...(source.instructions === undefined ? {} : { instructions: source.instructions }),
|
|
44
|
+
// `skills` 不写就是不进 config:缺省由这份定义自己的工具名单推导(写成 `false` 才落进 config)。
|
|
45
|
+
...(source.skills === undefined ? {} : { skills: source.skills }),
|
|
34
46
|
...(source.runtimeContext === undefined ? {} : { runtimeContext: source.runtimeContext }),
|
|
35
47
|
...(source.defaultModel === undefined ? {} : { defaultModel: { ...source.defaultModel } }),
|
|
36
48
|
};
|
|
37
49
|
}
|
|
38
50
|
|
|
39
|
-
|
|
51
|
+
// `session-mode` 行的装配:默认模式与各模式的扩展定义。
|
|
40
52
|
export function sessionModeRows(): readonly PatchRow[] {
|
|
41
53
|
return [
|
|
42
54
|
{
|