@morlay/dsh-session-mode 0.0.7 → 0.0.9
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 +27 -15
- package/dist/catalog-DWxIg3qw.mjs +131 -0
- package/dist/catalog-K5cESbqA.cjs +136 -0
- package/dist/catalog-remote-B7ZgYqun.cjs +28 -0
- package/dist/catalog-remote-KXJjDaN6.mjs +17 -0
- package/dist/client.cjs +1364 -16751
- package/dist/client.d.cts +149 -3
- package/dist/client.d.mts +149 -3
- package/dist/client.mjs +1612 -0
- package/dist/index.cjs +635 -0
- package/dist/index.d.mts +5 -3
- package/dist/index.mjs +73 -57
- package/dist/rows.cjs +85 -0
- package/dist/rows.mjs +40 -32
- package/dist/shared-sy45ixVR.cjs +16 -0
- package/dist/testing.cjs +6 -0
- package/dist/testing.d.mts +35 -0
- package/dist/testing.mjs +3 -0
- package/package.json +74 -50
- package/src/catalog-remote.ts +45 -0
- package/src/catalog.ts +75 -0
- package/src/client/BundleConfigPage.tsx +631 -0
- package/src/client/SessionModeSeat.tsx +21 -19
- package/src/client/bundle-config.ts +701 -0
- package/src/client/bundle-locales.ts +216 -0
- package/src/client/index.ts +178 -74
- package/src/client/llm-directory.ts +149 -0
- package/src/client/locales.ts +2 -2
- package/src/index.ts +28 -11
- package/src/mode-sources.ts +31 -14
- package/src/modes.ts +51 -25
- package/src/rows.ts +3 -2
- package/src/scope.ts +74 -65
- package/src/testing.ts +3 -0
- package/dist/index.d.cts +0 -144
- package/dist/rows.d.cts +0 -11
- package/src/client/SessionModeSeat.module.css +0 -74
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// 部署的 LLM 目录读数:provider 候选(活着的路由 + 可配置声明)与每个 provider 的模型清单(在声明指向的那份
|
|
2
|
+
// 配置里)。
|
|
3
|
+
//
|
|
4
|
+
// 两处消费它:提示面的 `llm-providers` / `llm-models` 两个具名源(行配置页与通用 schema 表单用),以及 bundle
|
|
5
|
+
// 配置页的模型路由清单——"已设置的模型"按**路由**给,一条路由就是 provider + model 这一对。
|
|
6
|
+
|
|
7
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
8
|
+
import type {} from "@deepseek-ai/dsh-api-remotes/client";
|
|
9
|
+
|
|
10
|
+
// 一条可选路由:`key` 是稳定身份(调用方只用来比对,不解析它)。
|
|
11
|
+
export interface ModelRoute {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly provider: string;
|
|
14
|
+
readonly providerName: string;
|
|
15
|
+
readonly model: string;
|
|
16
|
+
readonly modelName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 一个 provider 与它当前可用的模型。
|
|
20
|
+
export interface ProviderModels {
|
|
21
|
+
readonly value: string;
|
|
22
|
+
readonly label: string;
|
|
23
|
+
readonly models: readonly { readonly value: string; readonly label: string }[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 路由的稳定身份:provider 与 model 一起才是路由。
|
|
27
|
+
export function routeKey(provider: string, model: string): string {
|
|
28
|
+
return `${provider}\u0000${model}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 客户端 remote 的命名空间是一个**独立服务**(服务名 `remote.<命名空间>`,gateway 的 client 半按 mount 注册),
|
|
32
|
+
// 不是 `remote` 服务上的属性——属性访问读到的是 `undefined`。读不到(这一版部署没装那个命名空间)就按"没有"处理。
|
|
33
|
+
export function remoteNamespace<T>(scope: Context, namespace: string): T | undefined {
|
|
34
|
+
try {
|
|
35
|
+
return (scope as unknown as { get(key: string): unknown }).get(`remote.${namespace}`) as
|
|
36
|
+
| T
|
|
37
|
+
| undefined;
|
|
38
|
+
} catch {
|
|
39
|
+
// 服务没注册时 `ctx.get` 抛错:按"这个命名空间不在这一版部署里"处理。
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// LLM 命名空间里本包用到的那两个读面:活着的路由与可配置声明。
|
|
45
|
+
interface LlmReader {
|
|
46
|
+
listProviders: () => Promise<
|
|
47
|
+
{ ok: true; value: readonly { id: string; name: string }[] } | { ok: false }
|
|
48
|
+
>;
|
|
49
|
+
listConfigurableProviders: () => Promise<
|
|
50
|
+
| {
|
|
51
|
+
ok: true;
|
|
52
|
+
value: readonly {
|
|
53
|
+
provider: string;
|
|
54
|
+
displayName: string;
|
|
55
|
+
settingsNs: string;
|
|
56
|
+
settingsPath: readonly string[];
|
|
57
|
+
}[];
|
|
58
|
+
}
|
|
59
|
+
| { ok: false }
|
|
60
|
+
>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 一份 provider 档案的读数面(模型清单住在声明指向的那份配置里)。
|
|
64
|
+
interface ConfigFormsReader {
|
|
65
|
+
get(ns: string): { getSnapshot(): { value: unknown } };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 按路径读一段配置里的值。
|
|
69
|
+
function readAt(root: unknown, path: readonly string[]): unknown {
|
|
70
|
+
return path.reduce<unknown>((node, segment) => {
|
|
71
|
+
if (Array.isArray(node)) return node[Number(segment)];
|
|
72
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
73
|
+
return Reflect.get(node, segment);
|
|
74
|
+
}, root);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 一个 provider 当前有哪些模型:声明给了配置地址就按路径读它的 `models`(每项取 `id`)。
|
|
78
|
+
function modelsOf(
|
|
79
|
+
entry: { settingsNs: string; settingsPath: readonly string[] },
|
|
80
|
+
forms: ConfigFormsReader,
|
|
81
|
+
): readonly { value: string; label: string }[] {
|
|
82
|
+
if (entry.settingsNs === "") return [];
|
|
83
|
+
const profile = readAt(forms.get(entry.settingsNs).getSnapshot().value, entry.settingsPath);
|
|
84
|
+
const models =
|
|
85
|
+
typeof profile === "object" && profile !== null ? Reflect.get(profile, "models") : undefined;
|
|
86
|
+
if (!Array.isArray(models)) return [];
|
|
87
|
+
return models.flatMap((model) => {
|
|
88
|
+
if (typeof model !== "object" || model === null) return [];
|
|
89
|
+
const id = Reflect.get(model, "id");
|
|
90
|
+
if (typeof id !== "string") return [];
|
|
91
|
+
// 显示名优先取档案里的 `name`("DeepSeek V4.1 Flash @ Ollama Cloud" 这种),没有就用 id。
|
|
92
|
+
const name = Reflect.get(model, "name");
|
|
93
|
+
return [{ value: id, label: typeof name === "string" && name !== "" ? name : id }];
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 部署里"已设置"的 provider 与它们的模型:活着的路由与可配置声明合并(按 provider 去重,声明的显示名优先)。
|
|
98
|
+
export async function loadLlmProviders(scope: Context): Promise<readonly ProviderModels[]> {
|
|
99
|
+
const llm = remoteNamespace<LlmReader>(scope, "llm");
|
|
100
|
+
const forms = (scope as unknown as { get(key: string): unknown }).get("configForms") as
|
|
101
|
+
| ConfigFormsReader
|
|
102
|
+
| undefined;
|
|
103
|
+
if (llm === undefined || forms === undefined) return [];
|
|
104
|
+
const [routes, directory] = await Promise.all([
|
|
105
|
+
llm.listProviders(),
|
|
106
|
+
llm.listConfigurableProviders(),
|
|
107
|
+
]);
|
|
108
|
+
if (!routes.ok || !directory.ok) return [];
|
|
109
|
+
const merged = new Map<
|
|
110
|
+
string,
|
|
111
|
+
{ value: string; label: string; settingsNs: string; settingsPath: readonly string[] }
|
|
112
|
+
>();
|
|
113
|
+
for (const entry of directory.value) {
|
|
114
|
+
merged.set(entry.provider, {
|
|
115
|
+
value: entry.provider,
|
|
116
|
+
label: entry.displayName,
|
|
117
|
+
settingsNs: entry.settingsNs,
|
|
118
|
+
settingsPath: [...entry.settingsPath],
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
for (const route of routes.value) {
|
|
122
|
+
if (!merged.has(route.id)) {
|
|
123
|
+
merged.set(route.id, {
|
|
124
|
+
value: route.id,
|
|
125
|
+
label: route.name,
|
|
126
|
+
settingsNs: "",
|
|
127
|
+
settingsPath: [],
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return [...merged.values()].map((entry) => ({
|
|
132
|
+
value: entry.value,
|
|
133
|
+
label: entry.label,
|
|
134
|
+
models: modelsOf(entry, forms),
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 目录里的每一条路由(provider + 它有模型的每一项):bundle 配置页的模型清单用它。
|
|
139
|
+
export function routesOf(providers: readonly ProviderModels[]): readonly ModelRoute[] {
|
|
140
|
+
return providers.flatMap((provider) =>
|
|
141
|
+
provider.models.map((model) => ({
|
|
142
|
+
key: routeKey(provider.value, model.value),
|
|
143
|
+
provider: provider.value,
|
|
144
|
+
providerName: provider.label,
|
|
145
|
+
model: model.value,
|
|
146
|
+
modelName: model.label,
|
|
147
|
+
})),
|
|
148
|
+
);
|
|
149
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -11,7 +11,7 @@ export const zh = {
|
|
|
11
11
|
model: "模型",
|
|
12
12
|
modelHint: "模型 id。",
|
|
13
13
|
reasoningEffort: "思考档位",
|
|
14
|
-
reasoningEffortHint: "
|
|
14
|
+
reasoningEffortHint: "默认就跟服务商自己的默认。",
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export const en: Record<keyof typeof zh, string> = {
|
|
@@ -24,7 +24,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
24
24
|
model: "Model",
|
|
25
25
|
modelHint: "Model id.",
|
|
26
26
|
reasoningEffort: "Reasoning effort",
|
|
27
|
-
reasoningEffortHint: "
|
|
27
|
+
reasoningEffortHint: "Default keeps the provider's own default.",
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
export type SessionModeLocaleKey = keyof typeof zh;
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
type SessionMode,
|
|
26
26
|
type SessionModeRole,
|
|
27
27
|
} from "./modes.ts";
|
|
28
|
+
import { SessionModeCatalog } from "./catalog.ts";
|
|
28
29
|
import { installPersona } from "./persona.ts";
|
|
29
30
|
import { SessionScope } from "./scope.ts";
|
|
30
31
|
import {
|
|
@@ -263,8 +264,8 @@ export class SessionModes extends Service {
|
|
|
263
264
|
return undefined;
|
|
264
265
|
}
|
|
265
266
|
|
|
266
|
-
// 把某个空白会话(必须还没开过 turn
|
|
267
|
-
//
|
|
267
|
+
// 把某个空白会话(必须还没开过 turn)切到某个模式:先按模式的白名单把 agent preset 落到位(当前那份已经在
|
|
268
|
+
// 名单里就不换——换 preset 是一次重挂;不在名单里就换成名单里的第一个),再落会话事实并重装该 agent 的扩展。
|
|
268
269
|
async select(sessionId: SessionId, mode: string): Promise<string> {
|
|
269
270
|
const definition = this.definition(mode);
|
|
270
271
|
if (!definition.role.includes("main")) {
|
|
@@ -278,11 +279,12 @@ export class SessionModes extends Service {
|
|
|
278
279
|
}
|
|
279
280
|
const agent = this.ctx.agents.get(sessionId);
|
|
280
281
|
const registry = this.presetRegistry();
|
|
281
|
-
if (agent !== undefined && registry !== undefined
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
282
|
+
if (agent !== undefined && registry !== undefined) {
|
|
283
|
+
const target = this.presetTarget(definition.presetsOnly, this.presetOfAgent(registry, agent));
|
|
284
|
+
if (target !== undefined) {
|
|
285
|
+
// 官方那条路自己也会查空白窗口(`agent-preset/locked`);它切完会 emit `agent-preset/selected`,
|
|
286
|
+
// 监听据此落事实并重装——所以这里先比一次投影,同一个值不写第二条。
|
|
287
|
+
await registry.select(agent, target);
|
|
286
288
|
}
|
|
287
289
|
}
|
|
288
290
|
if (this.ctx.sessionProjections.stateOf(session, "sessionMode") !== mode) {
|
|
@@ -293,6 +295,19 @@ export class SessionModes extends Service {
|
|
|
293
295
|
return mode;
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
// 把当前挂着的 preset 落进模式的白名单:已经在名单里就返回 `undefined`(不换);不在里面(或读不到当前那份)
|
|
299
|
+
// 就返回名单的**第一个**(`undefined` = 名单为空 = 不限制,什么都不换)。
|
|
300
|
+
private presetTarget(
|
|
301
|
+
allowed: readonly string[],
|
|
302
|
+
current: string | undefined,
|
|
303
|
+
): string | undefined {
|
|
304
|
+
if (allowed.length === 0) return undefined;
|
|
305
|
+
// 当前那份在名单里:保持不动(用户自己选的 preset 落在许可范围内,模式不去覆盖它)。
|
|
306
|
+
if (current !== undefined && allowed.includes(current)) return undefined;
|
|
307
|
+
const target = allowed[0];
|
|
308
|
+
return target === current ? undefined : target;
|
|
309
|
+
}
|
|
310
|
+
|
|
296
311
|
// 某个 agent 当前挂着的 preset(registry 的 `composedPreset`);读不到时按"未知"处理(该切就切)。
|
|
297
312
|
private presetOfAgent(registry: Context["agentPresets"], agent: Agent): string | undefined {
|
|
298
313
|
const read = (registry as { composedPreset?: (ctx: Context) => string | undefined })
|
|
@@ -355,12 +370,12 @@ export class SessionModes extends Service {
|
|
|
355
370
|
}
|
|
356
371
|
}
|
|
357
372
|
|
|
358
|
-
// 某个 preset 对应的模式 id
|
|
359
|
-
//
|
|
373
|
+
// 某个 preset 对应的模式 id——只在映射唯一时回答(只有它一个模式的名单里有这份 preset;没有模式认领、或几个
|
|
374
|
+
// 模式的名单都含它时 `undefined`);本部署的模式由会话事实决定,这条反查留给"一对一映射"的部署形态。
|
|
360
375
|
modeForPreset(preset: string | undefined): string | undefined {
|
|
361
376
|
if (preset === undefined) return undefined;
|
|
362
|
-
const owners = Object.keys(this.config.modes).filter(
|
|
363
|
-
|
|
377
|
+
const owners = Object.keys(this.config.modes).filter((id) =>
|
|
378
|
+
this.config.modes[id]?.presetsOnly.includes(preset),
|
|
364
379
|
);
|
|
365
380
|
return owners.length === 1 ? owners[0] : undefined;
|
|
366
381
|
}
|
|
@@ -429,6 +444,8 @@ export class SessionModes extends Service {
|
|
|
429
444
|
export function apply(ctx: Context, config: ResolvedConfig): void {
|
|
430
445
|
const modes = new SessionModes(ctx, config);
|
|
431
446
|
registerHttpRoutes(ctx, modes);
|
|
447
|
+
// 配置页的工具 / 技能候选:一条本包自己开的 Remote 面(客户端半自己 mount,见 `./catalog-remote.ts`)。
|
|
448
|
+
ctx.plugin(SessionModeCatalog);
|
|
432
449
|
}
|
|
433
450
|
|
|
434
451
|
interface HttpRequestLike {
|
package/src/mode-sources.ts
CHANGED
|
@@ -7,10 +7,10 @@ import type { PolicyName } from "./shared.ts";
|
|
|
7
7
|
// 一个模式的源定义:就是 `session-mode` 行 `config.modes` 里的一项。
|
|
8
8
|
export interface ModeSource {
|
|
9
9
|
readonly id: string;
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
readonly
|
|
10
|
+
// 这个模式**只允许**挂哪些 agent preset(它们的 `id`):**可选**,不写(留空)就是不限制——选这个模式不换 preset,
|
|
11
|
+
// 会话保持它当前挂着的那份,行清单由那份 preset 提供(官方四个 shipped preset 照旧可选)。有值时切模式会让 preset
|
|
12
|
+
// 落进名单:当前的已经在里面就不动,否则换成第一个。模式定义里可以整体改写它(用户 patch 层)。
|
|
13
|
+
readonly presetsOnly?: readonly string[];
|
|
14
14
|
readonly name: string;
|
|
15
15
|
readonly description: string;
|
|
16
16
|
// 归谁用:`main`(用户选择器,缺省)、`subagent`(可作子代理 mode 的候选)。
|
|
@@ -44,13 +44,16 @@ export interface ModeModelSource {
|
|
|
44
44
|
readonly reasoningEffort?: string;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
// 编码模式:编程专家 +
|
|
47
|
+
// 编码模式:编程专家 + 语言与思考纪律。
|
|
48
|
+
// 这里**不再有**工作目录那半句(原为 `你的工作目录在 \`{{cwd}}\``):上游 0.2.1-alpha.2 起工作目录由
|
|
49
|
+
// `working-directory:current` 这条运行时上下文注入(文本是 `Current working directory: "<绝对路径>".`),
|
|
50
|
+
// 而 persona 用的提示词插值是**严格**的——`cwd` 变量已不再注册,残留 `{{cwd}}` 会在渲染时直接抛
|
|
51
|
+
// `unknown prompt variable`(注册变量只剩 provider / model)。
|
|
48
52
|
const CODING_PERSONA = {
|
|
49
53
|
prefix: [
|
|
50
54
|
"你是一个经验丰富的编程专家,YAGNI 是你的编程哲学,PDCA 是你的行为规范。",
|
|
51
55
|
"全程用中文(专有名词除外),包括但不限于思考,回答,工具描述,subagent 提示词;思考不要陷入重复循环,一旦循环立即退出;思考聚焦需求理解与方案设计,不预演具体代码实现,正确性由验证环节确认。",
|
|
52
56
|
].join("\n"),
|
|
53
|
-
suffix: "你的工作目录在 `{{cwd}}`",
|
|
54
57
|
};
|
|
55
58
|
|
|
56
59
|
// 对话模式:一个助手,保留语言与思考纪律,没有 suffix。
|
|
@@ -59,11 +62,16 @@ const CHAT_PERSONA = {
|
|
|
59
62
|
"你是一个助手。全程用中文(专有名词除外),包括但不限于思考,回答,工具描述;思考不要陷入重复循环,一旦循环立即退出。",
|
|
60
63
|
};
|
|
61
64
|
|
|
65
|
+
// 原样模式:**一份什么都不加的扩展**——persona 空、名单全空、三个开关不写,装配结果与"没有模式"时一致。
|
|
66
|
+
// 它存在的理由有两个:想完全按上游默认跑的会话有个可选项;排查扩展干扰时有一个对照档(切到它就只剩上游行为)。
|
|
67
|
+
// 它同样是 `session-mode` 行的一份定义,所以模式自己的收口代码照常经过它——只是没有任何收口要求。
|
|
68
|
+
const NOOP_MODE_ID = "noop";
|
|
69
|
+
|
|
62
70
|
// 新会话用哪个模式(`session-mode` 行的 `config.default`)。
|
|
63
71
|
export const DEFAULT_MODE = "coding";
|
|
64
72
|
|
|
65
|
-
//
|
|
66
|
-
// 不收窄(用全部),`chat` 收成提问 +
|
|
73
|
+
// 三个模式:编码、对话与原样。**都不限制 preset**(差异全在会话级收口),行清单由会话挂着的那份提供:`coding`
|
|
74
|
+
// 不收窄(用全部),`chat` 收成提问 + 联网三件,`noop` 什么都不加。
|
|
67
75
|
export const MODE_SOURCES: readonly ModeSource[] = [
|
|
68
76
|
{
|
|
69
77
|
id: "coding",
|
|
@@ -78,10 +86,7 @@ export const MODE_SOURCES: readonly ModeSource[] = [
|
|
|
78
86
|
// 官方 Office 技能的三个名字同样按会话排除:技能目录里不列它们,`skill` 工具加载它们也被拒。
|
|
79
87
|
// 名单是技能名这一层的模型可见契约(官方 `skill-office` 的 `SKILL_NAMES`),跟着上游改名走。
|
|
80
88
|
denySkills: ["office-docx", "office-pptx", "office-xlsx"],
|
|
81
|
-
|
|
82
|
-
// `denyPolicies` 只禁 `fs/edit-intent`(上游那条"先读后改"):改文件不再要求先读过——写路径上的
|
|
83
|
-
// `fs/write-intent`(陈旧版本 CAS 那层安全网)照旧生效,那正是这条配置不写成"两条都禁"的理由。
|
|
84
|
-
denyPolicies: ["fs/edit-intent"],
|
|
89
|
+
denyPolicies: ["fs/edit-intent", "fs/write-intent"],
|
|
85
90
|
},
|
|
86
91
|
{
|
|
87
92
|
id: "chat",
|
|
@@ -98,10 +103,22 @@ export const MODE_SOURCES: readonly ModeSource[] = [
|
|
|
98
103
|
// 配了只是噪音,所以留空。
|
|
99
104
|
// 没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
|
|
100
105
|
//
|
|
101
|
-
//
|
|
106
|
+
// 注入面全关:`instructions: false` 丢掉官方 `agent-instructions`(工作区指令)的注入;这个模式不写
|
|
102
107
|
// `skills`,而白名单三件里没有 `skill`、`denyTools` 也留空 → 推导成 `false`,官方 `skill-catalog` 的注入
|
|
103
|
-
//
|
|
108
|
+
// 同样丢掉;`runtimeContext: false` 连动态快照与时钟(`standard` 那类 preset 声明的 `time-context`)一起收。
|
|
109
|
+
// **收不掉的**:上游 `working-directory:current` 是 required 的目录上下文(0.2.1-alpha.2 起工作目录只走这条),
|
|
110
|
+
// 抑制可选运行时上下文不影响它——chat 会话的模型仍会看到工作目录那条快照。
|
|
111
|
+
// 取舍见 `.agents/designs/20260929-抑制官方注入面.md`。
|
|
104
112
|
instructions: false,
|
|
105
113
|
runtimeContext: false,
|
|
106
114
|
},
|
|
115
|
+
{
|
|
116
|
+
id: NOOP_MODE_ID,
|
|
117
|
+
name: "原样模式",
|
|
118
|
+
description: "与上游一致:不加人格提示词、不收窄工具与技能、policy 规则全开、官方注入面照旧。",
|
|
119
|
+
// 用户可选,也允许作为子代理的 mode:它不做任何过滤,给子代理当候选同样成立。
|
|
120
|
+
role: ["main", "subagent"],
|
|
121
|
+
// 其余字段一律不写:名单留空 = 不设收窄,`instructions` / `runtimeContext` 走 schema 默认 `true`,
|
|
122
|
+
// `skills` 由工具名单推导(`allowTools` 留空 → 含 `skill` → 要目录),`presetsOnly` 留空 = 不限制、`defaultModel` 跟全局。
|
|
123
|
+
},
|
|
107
124
|
];
|
package/src/modes.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// 模式的定义形状与它的装配期校验:一个模式就是"一段提示词 + 一组能力开关",本行的 `config.modes` 声明它们。
|
|
2
|
-
//
|
|
2
|
+
// 行清单来自会话挂着的那份 agent preset(`presetsOnly` 只声明这个模式允许挂哪些);装配层能整体改写 `modes`,
|
|
3
|
+
// 也可以只给某几个模式换提示词或白名单。
|
|
3
4
|
// 取舍见 `.agents/adrs/20260925-默认模型住在模式定义里.md`、`.agents/designs/20260924-会话模式.md`。
|
|
4
5
|
|
|
5
6
|
import type { Volatile } from "@deepseek-ai/cordis";
|
|
@@ -31,11 +32,12 @@ export type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
|
|
|
31
32
|
// 一个模式:提示词 + 能力开关。这份形状是 **schema 归一化之后**的(每个字段都有值,schema 用默认补上),
|
|
32
33
|
// 配置里能省略哪些字段看 `modeSchema` 的 default。
|
|
33
34
|
export interface SessionMode {
|
|
34
|
-
//
|
|
35
|
-
//
|
|
35
|
+
// 这个模式**只允许**挂哪些 **agent preset**(官方四个,或本部署自己注册的那一份)。**留空(不写)表示不限制**:
|
|
36
|
+
// 选这个模式不换 preset,会话保持它当前挂着的那份;行清单由那份 preset 提供,模式自己的那几项扩展
|
|
36
37
|
// (persona / 工具名单 / policy 名单 / 三个开关 / `defaultModel`)在任意 preset 上都照常生效。
|
|
37
|
-
//
|
|
38
|
-
|
|
38
|
+
// 有值时切到这个模式会让会话的 preset 落进名单:当前那份已经在名单里就原样不动,否则换成名单里的**第一个**
|
|
39
|
+
// (`modes.ts` 只声明"允许哪些",落在哪个由 `SessionModes.select` 决定)。多个模式可以共享同一份名单。
|
|
40
|
+
readonly presetsOnly: string[];
|
|
39
41
|
// 模式的展示名(选择面归官方 roster;这里留着做事实文案)。
|
|
40
42
|
readonly name: string;
|
|
41
43
|
// 一句话说明这个模式干什么;空串表示没写。
|
|
@@ -70,12 +72,15 @@ export interface SessionMode {
|
|
|
70
72
|
// denyTools` 里含 `skill` 就要(`allowTools` 留空即"全部",所以只有 `denyTools` 能把它推成 `false`)。
|
|
71
73
|
// `false` 表示这个模式不要技能目录:丢掉官方 `skill-catalog` 的注入(`skill` 工具的收窄仍归 `allowTools`)。
|
|
72
74
|
readonly skills?: boolean;
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
+
// 是否要动态快照(文件沙箱策略、审批策略那两条 snapshot section,以及 `standard` 那类 preset 声明的时钟
|
|
76
|
+
// `time-context` 注入)。缺省要;`false` 表示这个模式不要它们——对话模式没有文件与 shell 工具,"能改工作区
|
|
77
|
+
// 哪些文件、要不要走审批"对它全是噪音,它也不谈时间。
|
|
75
78
|
readonly runtimeContext: boolean;
|
|
76
79
|
// 这个模式的默认模型;省略就跟全局 `agent-default-model`。**可选**:没配的模式在页面上不出现在这一行
|
|
77
80
|
// (`defaultModel` 是它所在模式的一个可加字段)。
|
|
78
81
|
readonly defaultModel?: SessionModeModel;
|
|
82
|
+
// 退役字段:单个 `preset` 已换成白名单 `presetsOnly`。这里留着只为**报错**(见 `configProblem`),页面上不出现。
|
|
83
|
+
readonly preset?: string;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
// `skills` 不写时的推导:起点是白名单(留空 = 起点是全部工具,含 `skill`),减去黑名单,还留着 `skill` 就要技能
|
|
@@ -121,8 +126,8 @@ const modelSchema = z.object({
|
|
|
121
126
|
),
|
|
122
127
|
reasoningEffort: z.string().description(
|
|
123
128
|
localized({
|
|
124
|
-
zh: "
|
|
125
|
-
en: "Reasoning effort;
|
|
129
|
+
zh: "思考档位;默认跟服务商自己的默认。",
|
|
130
|
+
en: "Reasoning effort; the default is the provider's own.",
|
|
126
131
|
}),
|
|
127
132
|
),
|
|
128
133
|
});
|
|
@@ -149,13 +154,17 @@ export interface ResolvedConfig {
|
|
|
149
154
|
}
|
|
150
155
|
|
|
151
156
|
const modeSchema: z<SessionMode> = z.object({
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
// 退役字段:这里留着只为**报错**(见 `configProblem`),页面上不出现(`hidden()`)。不给默认:没配就没这个键。
|
|
158
|
+
preset: z.string().hidden(),
|
|
159
|
+
presetsOnly: z
|
|
160
|
+
.array(z.string())
|
|
161
|
+
// 候选是部署里注册的 agent preset(具名源 `agent-presets`):字段只说"这是选几个",不关心它从哪来。
|
|
162
|
+
.role("select", { source: "agent-presets" })
|
|
163
|
+
.default([])
|
|
155
164
|
.description(
|
|
156
165
|
localized({
|
|
157
|
-
zh: "
|
|
158
|
-
en: "Which agent
|
|
166
|
+
zh: "这个模式只允许挂哪些 agent preset(它们的 id,官方或本部署自建的):行清单由挂着的那份提供,几个模式可以共享同一份名单。留空表示**允许全部**——哪份 preset 都行,选这个模式也不动会话当前挂着的那份;有值时当前的已经在名单里就不动,否则换成名单里的第一个。模式自己的提示词、工具收口与开关照常生效。",
|
|
167
|
+
en: "Which agent presets this mode may ride on (their ids, shipped or deployment-owned): that preset supplies the row list, and several modes may share one list. Empty means **every preset is allowed** — selecting the mode then leaves the session's current preset alone; when set, the current one stays if it is listed, otherwise the session switches to the first entry. The mode's persona, tool narrowing, and switches apply either way.",
|
|
159
168
|
}),
|
|
160
169
|
),
|
|
161
170
|
name: z
|
|
@@ -188,6 +197,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
188
197
|
persona: personaSchema.default({}),
|
|
189
198
|
allowTools: z
|
|
190
199
|
.array(z.string())
|
|
200
|
+
// 候选是部署里的工具名(具名源 `catalog-tools`,由 client 半注册):字段只说"这是选几个",不关心清单从哪来。
|
|
201
|
+
.role("select", { source: "catalog-tools" })
|
|
191
202
|
.default([])
|
|
192
203
|
.description(
|
|
193
204
|
localized({
|
|
@@ -197,6 +208,7 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
197
208
|
),
|
|
198
209
|
denyTools: z
|
|
199
210
|
.array(z.string())
|
|
211
|
+
.role("select", { source: "catalog-tools" })
|
|
200
212
|
.default([])
|
|
201
213
|
.description(
|
|
202
214
|
localized({
|
|
@@ -206,6 +218,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
206
218
|
),
|
|
207
219
|
allowSkills: z
|
|
208
220
|
.array(z.string())
|
|
221
|
+
// 候选是技能注册表里的名字(具名源 `catalog-skills`)。
|
|
222
|
+
.role("select", { source: "catalog-skills" })
|
|
209
223
|
.default([])
|
|
210
224
|
.description(
|
|
211
225
|
localized({
|
|
@@ -215,6 +229,7 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
215
229
|
),
|
|
216
230
|
denySkills: z
|
|
217
231
|
.array(z.string())
|
|
232
|
+
.role("select", { source: "catalog-skills" })
|
|
218
233
|
.default([])
|
|
219
234
|
.description(
|
|
220
235
|
localized({
|
|
@@ -224,6 +239,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
224
239
|
),
|
|
225
240
|
allowPolicies: z
|
|
226
241
|
.array(z.string())
|
|
242
|
+
// 候选是上游那两条 policy 名(具名源 `policies`)。
|
|
243
|
+
.role("select", { source: "policies" })
|
|
227
244
|
.default([])
|
|
228
245
|
.description(
|
|
229
246
|
localized({
|
|
@@ -233,6 +250,7 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
233
250
|
),
|
|
234
251
|
denyPolicies: z
|
|
235
252
|
.array(z.string())
|
|
253
|
+
.role("select", { source: "policies" })
|
|
236
254
|
.default([])
|
|
237
255
|
.description(
|
|
238
256
|
localized({
|
|
@@ -251,8 +269,8 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
251
269
|
),
|
|
252
270
|
skills: z.boolean().description(
|
|
253
271
|
localized({
|
|
254
|
-
zh: "是否要技能目录(官方 `skill-catalog`
|
|
255
|
-
en: "Whether the skill catalog applies (the official `skill-catalog` injection).
|
|
272
|
+
zh: "是否要技能目录(官方 `skill-catalog` 的注入)。**默认就按这个模式自己的工具名单推导**:`(allowTools 留空 ? 全部 : allowTools) − denyTools` 里含 `skill` 就要;写 `false` 就丢掉官方 `skill-catalog` 的注入(`skill` 工具的可见性仍归 `allowTools`)。",
|
|
273
|
+
en: "Whether the skill catalog applies (the official `skill-catalog` injection). The default 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`).",
|
|
256
274
|
}),
|
|
257
275
|
),
|
|
258
276
|
runtimeContext: z
|
|
@@ -260,21 +278,20 @@ const modeSchema: z<SessionMode> = z.object({
|
|
|
260
278
|
.default(true)
|
|
261
279
|
.description(
|
|
262
280
|
localized({
|
|
263
|
-
zh: "
|
|
264
|
-
en: "Whether the runtime snapshot
|
|
281
|
+
zh: "是否要动态快照(文件沙箱策略、审批策略)与时钟注入(`standard` 那类 preset 声明的 `time-context`)。",
|
|
282
|
+
en: "Whether the runtime snapshot (sandbox and approval policy) and the clock injection (`time-context`, declared by presets such as `standard`) apply.",
|
|
265
283
|
}),
|
|
266
284
|
),
|
|
267
285
|
// 这个模式的默认模型。不标 `volatile`:`modes` 本身就是 volatile,整棵子树都在页面上——再标一层会被
|
|
268
286
|
// schemastery 拒(`validateVolatileSchema` 不许 volatile 套 volatile)。
|
|
269
|
-
defaultModel: modelSchema
|
|
270
|
-
// `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
|
|
287
|
+
defaultModel: modelSchema // `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
|
|
271
288
|
// 凭空多出一行;给了 null 反而让它保持缺失(页面按非必填处理,从候选加成)。
|
|
272
289
|
// 类型上放行一次:`null` 在这里只是"没有这个键"的写法,schema 的输入形状不接受它。
|
|
273
290
|
.default(null as unknown as SessionModeModel)
|
|
274
291
|
.description(
|
|
275
292
|
localized({
|
|
276
|
-
zh: "
|
|
277
|
-
en: "Default model for this mode;
|
|
293
|
+
zh: "这个模式的默认模型;不设就跟全局默认模型。只在会话还没有模型事实时接管。",
|
|
294
|
+
en: "Default model for this mode; empty follows the global default. Applies only while a session has no model fact yet.",
|
|
278
295
|
}),
|
|
279
296
|
),
|
|
280
297
|
});
|
|
@@ -316,7 +333,9 @@ interface Validated {
|
|
|
316
333
|
Record<
|
|
317
334
|
string,
|
|
318
335
|
{
|
|
319
|
-
//
|
|
336
|
+
// 留空合法的"不限制";允许共享(差异由会话级收口表达),所以这里不做任何映射唯一性校验。
|
|
337
|
+
readonly presetsOnly?: readonly string[];
|
|
338
|
+
// 退役的单个 preset:还配着值就报错(它已经不再生效)。
|
|
320
339
|
readonly preset?: string;
|
|
321
340
|
// 留空合法:不设收窄(用 preset 的全部工具)。
|
|
322
341
|
readonly allowTools?: readonly string[];
|
|
@@ -369,11 +388,18 @@ export function configProblem(config: Validated): string | undefined {
|
|
|
369
388
|
return `session-mode: mode(s) ${unknownPolicies.join(", ")} name unknown policies; the known ones are ${POLICY_NAMES.join(", ")}`;
|
|
370
389
|
}
|
|
371
390
|
// `allowTools` **留空是合法的**:不设收窄,用这个会话挂着的 preset 的全部工具。
|
|
372
|
-
// `
|
|
373
|
-
//
|
|
391
|
+
// `presetsOnly` **允许留空**(不限制会话挂哪份 preset)**也允许共享**(共享时 preset → 模式的反查交给
|
|
392
|
+
// `SessionModes.modeForPreset`);退役的顶层 `models` 还配着值就报错——别让一份"看着像配过"的配置静静地失效。
|
|
374
393
|
if (Object.keys(config.models ?? {}).length > 0) {
|
|
375
394
|
return "session-mode: `models` has moved into each mode's `defaultModel`; move the entries there and drop the top-level `models`";
|
|
376
395
|
}
|
|
396
|
+
// 退役的单个 `preset`(现在是白名单 `presetsOnly`):旧写法照旧"看着像配过"却不再生效,所以装配期拒绝。
|
|
397
|
+
const retiredPresets = Object.entries(config.modes)
|
|
398
|
+
.filter(([, mode]) => mode.preset !== undefined)
|
|
399
|
+
.map(([id]) => id);
|
|
400
|
+
if (retiredPresets.length > 0) {
|
|
401
|
+
return `session-mode: mode(s) ${retiredPresets.join(", ")} still declare \`preset\`; it has moved to \`presetsOnly\` (the presets that mode may ride on)`;
|
|
402
|
+
}
|
|
377
403
|
// 每个模式自己的默认模型:成对给全(`provider` 与 `model` 都要)。
|
|
378
404
|
const partial = Object.entries(config.modes)
|
|
379
405
|
.filter(([, mode]) => {
|
package/src/rows.ts
CHANGED
|
@@ -22,6 +22,7 @@ function list<T>(values: readonly T[] | undefined): readonly T[] | undefined {
|
|
|
22
22
|
|
|
23
23
|
// 一个模式的源定义 → 行 config(schema 的输入形状:可省的字段就省)。
|
|
24
24
|
function modeConfig(source: ModeSource): Record<string, unknown> {
|
|
25
|
+
const presetsOnly = list(source.presetsOnly);
|
|
25
26
|
const allowTools = list(source.allowTools);
|
|
26
27
|
const denyTools = list(source.denyTools);
|
|
27
28
|
const allowSkills = list(source.allowSkills);
|
|
@@ -29,8 +30,8 @@ function modeConfig(source: ModeSource): Record<string, unknown> {
|
|
|
29
30
|
const allowPolicies = list(source.allowPolicies);
|
|
30
31
|
const denyPolicies = list(source.denyPolicies);
|
|
31
32
|
return {
|
|
32
|
-
// `
|
|
33
|
-
...(
|
|
33
|
+
// `presetsOnly` 是可选的:不写(或写成空数组)就是这个键不进 config,schema 默认空数组(= 不限制)。
|
|
34
|
+
...(presetsOnly === undefined ? {} : { presetsOnly }),
|
|
34
35
|
name: source.name,
|
|
35
36
|
description: source.description,
|
|
36
37
|
...(source.role === undefined ? {} : { role: [...source.role] }),
|