@morlay/dsh-session-mode 0.0.2 → 0.0.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/README.md +51 -110
- package/dist/client.cjs +17 -71
- package/dist/client.d.cts +4 -19
- package/dist/client.d.mts +4 -19
- package/dist/index.d.cts +19 -180
- package/dist/index.d.mts +19 -180
- package/dist/index.mjs +149 -162
- package/dist/rows.d.cts +4 -31
- package/dist/rows.d.mts +4 -31
- 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 +10 -8
- 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/mode-sources.ts
CHANGED
|
@@ -1,59 +1,46 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* "自定义"就落在这份数据上——装配层(profile 的用户 patch 层)可以整体改写 `config.modes`,也可以只给
|
|
5
|
-
* 某个模式换提示词或工具白名单,不需要任何插件行。
|
|
6
|
-
*
|
|
7
|
-
* `allowTools` 从 [`@morlay/dsh-agent-toolkit/rows`](../../../dsh-agent-toolkit/src/rows.ts) 的
|
|
8
|
-
* `TOOLKIT_TOOL_NAMES` 派生(工具名与汉化同源):**行清单由本部署自己注册的那份 preset 提供**
|
|
9
|
-
* ({@link MODE_PRESET_ID},两个模式共享它),白名单里那份 preset 没有的工具自动跳过。想改某一个模式的名单,
|
|
10
|
-
* 直接在这条源数据里加/减。
|
|
11
|
-
*/
|
|
1
|
+
// 模式定义的真源:`session-mode` 行的 `config` 由 `./rows.ts` 渲染(装配入口在 `packages/bundles/session-mode-profile`)。
|
|
2
|
+
// **行清单不由本包持有**:谁挂在这份定义上的会话用什么工具,取决于它挂着的 agent preset(官方 shipped preset),
|
|
3
|
+
// `allowTools` 只是在这之上收窄——留空就是不收窄。改某个模式的名单就改这条源数据。
|
|
12
4
|
|
|
13
|
-
import {
|
|
5
|
+
import type { PolicyName } from "./shared.ts";
|
|
14
6
|
|
|
15
|
-
|
|
16
|
-
* 本部署自己注册的那份 agent preset 的 id:两个模式共享它(行清单见
|
|
17
|
-
* `packages/bundles/session-mode-profile`,`config.plugins` 引用 `@morlay/dsh-agent-toolkit/rows` 的
|
|
18
|
-
* `TOOLKIT_PRESET_ROWS`)。
|
|
19
|
-
*
|
|
20
|
-
* 为什么自己注册而不是复用官方 preset:官方 `minimal` 没有 `tool-web`,`chat` 的白名单(提问 + 联网三件)
|
|
21
|
-
* 收口后一件都不剩;而且官方 preset 自带的上游注入要在我们的开关之外做"让位",抢 `skill` 面还得把目录 kind
|
|
22
|
-
* 换掉。行清单归我们之后,两个模式的**差异全在会话级收口**(persona / `allowTools` / 两个开关)。
|
|
23
|
-
*/
|
|
24
|
-
export const MODE_PRESET_ID = "mode-switch";
|
|
25
|
-
|
|
26
|
-
/** 一个模式的源定义:就是 `session-mode` 行 `config.modes` 里的一项。 */
|
|
7
|
+
// 一个模式的源定义:就是 `session-mode` 行 `config.modes` 里的一项。
|
|
27
8
|
export interface ModeSource {
|
|
28
9
|
readonly id: string;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
* {@link MODE_PRESET_ID};两个模式共享同一个 preset 是**有意**的(差异靠会话级收口表达),所以
|
|
34
|
-
* preset → 模式的反查在这种共享下无意义(见 `SessionModes.modeForPreset`)。
|
|
35
|
-
*/
|
|
36
|
-
readonly preset: string;
|
|
10
|
+
// 挂哪个 agent preset(它的 `id`):**可选**,不写就是不绑——选这个模式不换 preset,会话保持它当前挂着的那份,
|
|
11
|
+
// 行清单由那份 preset 提供(官方四个 shipped preset 照旧可选)。写了才在切模式时把 preset 切过去;
|
|
12
|
+
// 模式定义里可以整体改写它(用户 patch 层)。
|
|
13
|
+
readonly preset?: string;
|
|
37
14
|
readonly name: string;
|
|
38
15
|
readonly description: string;
|
|
39
|
-
|
|
16
|
+
// 归谁用:`main`(用户选择器,缺省)、`subagent`(可作子代理 mode 的候选)。
|
|
40
17
|
readonly role?: readonly ("main" | "subagent")[];
|
|
41
18
|
readonly persona?: { readonly prefix?: string; readonly suffix?: string };
|
|
42
|
-
|
|
19
|
+
// 这个模式能用的工具;**可选**,不写(留空)就是不设收窄——用会话挂着的 preset 的全部工具。
|
|
20
|
+
readonly allowTools?: readonly string[];
|
|
21
|
+
// 这个模式不用的工具(黑名单):从 `allowTools` 定的那份里减掉(deny 优先)。
|
|
22
|
+
readonly denyTools?: readonly string[];
|
|
23
|
+
// 上游 policy 规则的生效白名单:**可选**,不写(留空)= 全部规则生效。
|
|
24
|
+
readonly allowPolicies?: readonly PolicyName[];
|
|
25
|
+
// 上游 policy 规则的黑名单:列出的规则禁用(它在上游那条 waterfall 上的裁决被绕过)。
|
|
26
|
+
readonly denyPolicies?: readonly PolicyName[];
|
|
43
27
|
readonly instructions?: boolean;
|
|
28
|
+
// 是否要技能目录(官方 `skill-catalog` 的注入):**可选**,不写就按这个模式自己的工具名单推导(名单里含 `skill`
|
|
29
|
+
// 就要,见 `modes.ts` 的 `derivedSkills`)。`coding` 留空名单 = 要;`chat` 的三件里没有 `skill` = 不要。
|
|
30
|
+
readonly skills?: boolean;
|
|
44
31
|
readonly runtimeContext?: boolean;
|
|
45
|
-
|
|
32
|
+
// 这个模式的默认模型(省略就跟全局 `agent-default-model`)。
|
|
46
33
|
readonly defaultModel?: ModeModelSource;
|
|
47
34
|
}
|
|
48
35
|
|
|
49
|
-
|
|
36
|
+
// 一个模式的默认模型源定义:`modes.<模式 id>.defaultModel`。
|
|
50
37
|
export interface ModeModelSource {
|
|
51
38
|
readonly provider: string;
|
|
52
39
|
readonly model: string;
|
|
53
40
|
readonly reasoningEffort?: string;
|
|
54
41
|
}
|
|
55
42
|
|
|
56
|
-
|
|
43
|
+
// 编码模式:编程专家 + 语言与思考纪律 + 工作目录提醒。
|
|
57
44
|
const CODING_PERSONA = {
|
|
58
45
|
prefix: [
|
|
59
46
|
"你是一个经验丰富的编程专家,YAGNI 是你的编程哲学,PDCA 是你的行为规范。",
|
|
@@ -62,41 +49,50 @@ const CODING_PERSONA = {
|
|
|
62
49
|
suffix: "你的工作目录在 `{{cwd}}`",
|
|
63
50
|
};
|
|
64
51
|
|
|
65
|
-
|
|
52
|
+
// 对话模式:一个助手,保留语言与思考纪律,没有 suffix。
|
|
66
53
|
const CHAT_PERSONA = {
|
|
67
54
|
prefix:
|
|
68
55
|
"你是一个助手。全程用中文(专有名词除外),包括但不限于思考,回答,工具描述;思考不要陷入重复循环,一旦循环立即退出。",
|
|
69
56
|
};
|
|
70
57
|
|
|
71
|
-
|
|
58
|
+
// 新会话用哪个模式(`session-mode` 行的 `config.default`)。
|
|
72
59
|
export const DEFAULT_MODE = "coding";
|
|
73
60
|
|
|
74
|
-
|
|
61
|
+
// 两个模式:编码与对话。**都不绑定 preset**(差异全在会话级收口),行清单由会话挂着的 preset 提供:`coding`
|
|
62
|
+
// 不收窄(用全部),`chat` 收成提问 + 联网三件。
|
|
75
63
|
export const MODE_SOURCES: readonly ModeSource[] = [
|
|
76
64
|
{
|
|
77
65
|
id: "coding",
|
|
78
|
-
// 完整工具集由我们自己的 preset(`TOOLKIT_PRESET_ROWS`)提供。
|
|
79
|
-
preset: MODE_PRESET_ID,
|
|
80
66
|
name: "编码模式",
|
|
81
67
|
description: "功能完整的编码 Agent:文件、Shell、检索、联网等工具常驻,其余用法说明按需加载。",
|
|
82
68
|
persona: CODING_PERSONA,
|
|
83
69
|
// 用户可选,也允许作为子代理的 mode(子代理默认继承父 mode,不看角色;这里是"可被指定"的候选集)。
|
|
84
70
|
role: ["main", "subagent"],
|
|
85
|
-
allowTools
|
|
71
|
+
// 不写 `allowTools`:不设收窄——这个会话用它挂着的 preset 提供的全部工具(抄一份清单只会与行清单漂移)。
|
|
72
|
+
// 不写 `denyTools`:一件工具都不禁。
|
|
73
|
+
//
|
|
74
|
+
// `denyPolicies` 只禁 `fs/edit-intent`(上游那条"先读后改"):改文件不再要求先读过——写路径上的
|
|
75
|
+
// `fs/write-intent`(陈旧版本 CAS 那层安全网)照旧生效,那正是这条配置不写成"两条都禁"的理由。
|
|
76
|
+
denyPolicies: ["fs/edit-intent"],
|
|
86
77
|
},
|
|
87
78
|
{
|
|
88
79
|
id: "chat",
|
|
89
|
-
//
|
|
90
|
-
preset: MODE_PRESET_ID,
|
|
80
|
+
// 行清单同样跟着会话:白名单里 preset 没有的工具自动跳过,所以这个模式在缺联网行的 preset 上收不出三件。
|
|
91
81
|
name: "对话模式",
|
|
92
82
|
description:
|
|
93
83
|
"只做对话:提问与联网(搜索、抓取)三件工具,不注入系统提示词、工作区指令与技能目录。",
|
|
94
84
|
persona: CHAT_PERSONA,
|
|
95
85
|
// 只做用户侧对话:不做子代理的候选(父在 chat 里派发的子代理仍继承 chat,见 README 的"角色"一节)。
|
|
96
86
|
role: ["main"],
|
|
97
|
-
//
|
|
87
|
+
// 提问与联网三件:行由会话挂着的 preset 提供,这里只收口(preset 没有的自动跳过)。
|
|
98
88
|
allowTools: ["ask_user_question", "web_search", "web_fetch"],
|
|
89
|
+
// 不写任何 policy 名单:上游两条规则都照旧生效。这个模式没有文件工具,两条都碰不到——
|
|
90
|
+
// 配了只是噪音,所以留空。
|
|
99
91
|
// 没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
|
|
92
|
+
//
|
|
93
|
+
// 两个抑制面都关:`instructions: false` 丢掉官方 `agent-instructions`(工作区指令)的注入;这个模式不写
|
|
94
|
+
// `skills`,而白名单三件里没有 `skill`、`denyTools` 也留空 → 推导成 `false`,官方 `skill-catalog` 的注入
|
|
95
|
+
// 同样丢掉。取舍见 `.agents/designs/20260929-抑制官方注入面.md`。
|
|
100
96
|
instructions: false,
|
|
101
97
|
runtimeContext: false,
|
|
102
98
|
},
|
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 = [
|