@morlay/dsh-session-mode 0.0.8 → 0.0.10

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.
@@ -28,9 +28,9 @@ import {
28
28
  TagInput,
29
29
  Text,
30
30
  type SettingsFormLabels,
31
- } from "@morlay/dsh-client-ui-primitives/client";
31
+ } from "@morlay/dsh-client-ui-primitives";
32
32
  import type { InjectFace, PropsLocale, PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
33
- import type { SchemaFormState } from "@morlay/dsh-client-ui-primitives/client";
33
+ import type { SchemaFormState } from "@morlay/dsh-client-ui-primitives";
34
34
  import {
35
35
  SESSION_MODE_NS,
36
36
  projectBundleConfig,
@@ -137,7 +137,9 @@ export function BundleConfigPage(props: BundleConfigPageProps): ReactNode {
137
137
  label={t("default.label")}
138
138
  hint={t("default.hint")}
139
139
  layout="inline"
140
- {...(state.defaultMode.invalid === undefined ? {} : { invalid: state.defaultMode.invalid })}
140
+ {...(state.defaultMode.invalid === undefined
141
+ ? {}
142
+ : { invalid: state.defaultMode.invalid })}
141
143
  data-field="default"
142
144
  data-control="choice"
143
145
  >
@@ -308,12 +310,7 @@ function ModeCard({
308
310
  }): ReactNode {
309
311
  const hasModel = mode.model !== undefined;
310
312
  return (
311
- <Stack
312
- boxed
313
- pad="card"
314
- data-mode={mode.id}
315
- data-deletable={mode.deletable ? "true" : "false"}
316
- >
313
+ <Stack boxed pad="card" data-mode={mode.id} data-deletable={mode.deletable ? "true" : "false"}>
317
314
  <DisclosureRow
318
315
  icon={<IconAgentPresetOutlineRegular />}
319
316
  title={mode.title}
@@ -329,7 +326,9 @@ function ModeCard({
329
326
  {mode.id}
330
327
  </Text>
331
328
  <Row gap={8} push>
332
- <Text as="span" size="sm" tone="tertiary">{mode.summary}</Text>
329
+ <Text as="span" size="sm" tone="tertiary">
330
+ {mode.summary}
331
+ </Text>
333
332
  {mode.deletable ? (
334
333
  <IconButton
335
334
  size="sm"
@@ -357,7 +356,9 @@ function ModeCard({
357
356
  {mode.groups.map((group) => (
358
357
  <Stack key={group.key} gap={0} data-group={group.key}>
359
358
  <Row gap={8} justify="between">
360
- <Text size="sm" tone="secondary" weight="strong">{group.label}</Text>
359
+ <Text size="sm" tone="secondary" weight="strong">
360
+ {group.label}
361
+ </Text>
361
362
  </Row>
362
363
  {group.key === "model" ? (
363
364
  <>
@@ -10,7 +10,7 @@ import {
10
10
  SeatButton,
11
11
  Stack,
12
12
  Text,
13
- } from "@morlay/dsh-client-ui-primitives/client";
13
+ } from "@morlay/dsh-client-ui-primitives";
14
14
  import type { PropsLocale, PropsRuntime } from "@deepseek-ai/dsh-client-ui-slots";
15
15
  // Type-only:拉入本包 host 半的 Context / 投影声明(`sessionMode`),让 `projectionValues` 有类型。
16
16
  import type {} from "../index.ts";
@@ -19,7 +19,7 @@ import {
19
19
  type SelectOption,
20
20
  type SettingsFormShell,
21
21
  type ValidationFailure,
22
- } from "@morlay/dsh-client-ui-primitives/client";
22
+ } from "@morlay/dsh-client-ui-primitives";
23
23
  import type { BundleLocaleKey } from "./bundle-locales.ts";
24
24
  import { loadLlmProviders, routesOf, type ModelRoute } from "./llm-directory.ts";
25
25
 
@@ -64,7 +64,8 @@ export const bundleZh = {
64
64
  "hint.instructions": "每步都把工作区指令(AGENTS.md 之类)发给模型;关掉就不发。",
65
65
  "hint.skills": "每步都把技能目录发给模型;默认按这个模式的工具名单自动判断。",
66
66
  "hint.runtimeContext": "每步都把沙箱与审批策略发给模型。",
67
- "hint.defaultModel": "开 = 这个模式自带一个默认模型(会话还没选过模型时用它);关 = 跟随全局模型。",
67
+ "hint.defaultModel":
68
+ "开 = 这个模式自带一个默认模型(会话还没选过模型时用它);关 = 跟随全局模型。",
68
69
  "hint.modelRoute": "从部署里已设置的模型里挑一条:服务商与模型是一对,选它等于两个一起定。",
69
70
  "hint.reasoningEffort": "思考档位;不填用服务商默认。",
70
71
  "model.loading": "正在读部署里已设置的模型…",
@@ -115,7 +116,8 @@ export const bundleEn: Record<keyof typeof bundleZh, string> = {
115
116
  "add.confirm": "Add",
116
117
  "add.cancel": "Cancel",
117
118
  "add.close": "Close",
118
- "add.hint": "Give the new mode an id (it also shows in pickers and lists; rename it after adding).",
119
+ "add.hint":
120
+ "Give the new mode an id (it also shows in pickers and lists; rename it after adding).",
119
121
  removeNamed: "Delete mode {name}",
120
122
  protected: "Built-in mode: no delete",
121
123
  untitled: "(untitled)",
@@ -11,8 +11,11 @@ import type {} from "@deepseek-ai/dsh-client-ui-slots";
11
11
  import type {} from "@deepseek-ai/dsh-client-ui-conversation/client";
12
12
  // Type-only:`plugins.bundle.config` 的槽位声明(bundle 详情页的配置座位)。
13
13
  import type {} from "@deepseek-ai/dsh-client-ui-plugin-manager/client";
14
- import { apply as installUiPrimitives } from "@morlay/dsh-client-ui-primitives/client";
15
- import { CATALOG_NS, SESSION_MODE_CATALOG_REMOTE, type SessionModeCatalogRemote } from "../catalog-remote.ts";
14
+ import {
15
+ CATALOG_NS,
16
+ SESSION_MODE_CATALOG_REMOTE,
17
+ type SessionModeCatalogRemote,
18
+ } from "../catalog-remote.ts";
16
19
  import { POLICY_NAMES } from "../shared.ts";
17
20
  import {
18
21
  BUNDLE_CONFIG_KEY,
@@ -24,11 +27,7 @@ import {
24
27
  } from "./bundle-config.ts";
25
28
  import { BundleConfigPage } from "./BundleConfigPage.tsx";
26
29
  import { BUNDLE_NS, bundleEn, bundleZh, type BundleLocaleKey } from "./bundle-locales.ts";
27
- import {
28
- loadLlmProviders,
29
- remoteNamespace,
30
- type ProviderModels,
31
- } from "./llm-directory.ts";
30
+ import { loadLlmProviders, remoteNamespace, type ProviderModels } from "./llm-directory.ts";
32
31
  import { SessionModeSeat } from "./SessionModeSeat.tsx";
33
32
  import { en, zh, type SessionModeLocaleKey } from "./locales.ts";
34
33
 
@@ -58,8 +57,7 @@ const DYNAMIC = "*";
58
57
  // 部署里的 agent preset 清单(`remote.agentPresets.list()`):本包只用它的 id 与显示名做候选。
59
58
  interface PresetRosterReader {
60
59
  list: () => Promise<
61
- | { ok: true; value: { presets: readonly { id: string; name?: string }[] } }
62
- | { ok: false }
60
+ { ok: true; value: { presets: readonly { id: string; name?: string }[] } } | { ok: false }
63
61
  >;
64
62
  }
65
63
 
@@ -83,7 +81,6 @@ function unwrapHintService(scope: Context): HintsService {
83
81
  // 装上会话里的那一个面、本行配置页的字段文案,以及装本行的 bundle 那一页的表单。
84
82
  export function apply(ctx: Context): void {
85
83
  // 基础面随本包 inline(不再是装配行):装上它提供的字典、字段槽与按行配置页;多份副本只装一次。
86
- installUiPrimitives(ctx);
87
84
  const t = ctx.locale.bind(NS);
88
85
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-mode: dictionaries");
89
86
 
@@ -162,7 +159,6 @@ export function apply(ctx: Context): void {
162
159
  }, "session-mode: model candidates"),
163
160
  );
164
161
 
165
-
166
162
  // 模式"允许挂哪些 preset"的候选:部署里注册的 agent preset(`remote.agentPresets.list()`),登记成具名源
167
163
  // `agent-presets`——schema 上 `presetsOnly` 声明 `role('select', { source })` 认领它,页面不必知道这些 preset
168
164
  // 从哪来。装配组合变了(设置文档一次更新)就重取一次。
@@ -220,7 +216,7 @@ export function apply(ctx: Context): void {
220
216
  }
221
217
  void scope.remote
222
218
  .$mount(SESSION_MODE_CATALOG_REMOTE)
223
- .then(async (dispose) => {
219
+ .then(async (dispose: () => Promise<void>) => {
224
220
  disposeMount = dispose;
225
221
  const listed = await remoteNamespace<SessionModeCatalogRemote>(scope, CATALOG_NS)?.list();
226
222
  if (listed?.ok !== true) return;
@@ -97,16 +97,19 @@ function modelsOf(
97
97
  // 部署里"已设置"的 provider 与它们的模型:活着的路由与可配置声明合并(按 provider 去重,声明的显示名优先)。
98
98
  export async function loadLlmProviders(scope: Context): Promise<readonly ProviderModels[]> {
99
99
  const llm = remoteNamespace<LlmReader>(scope, "llm");
100
- const forms = (scope as unknown as { get(key: string): unknown }).get(
101
- "configForms",
102
- ) as ConfigFormsReader | undefined;
100
+ const forms = (scope as unknown as { get(key: string): unknown }).get("configForms") as
101
+ | ConfigFormsReader
102
+ | undefined;
103
103
  if (llm === undefined || forms === undefined) return [];
104
104
  const [routes, directory] = await Promise.all([
105
105
  llm.listProviders(),
106
106
  llm.listConfigurableProviders(),
107
107
  ]);
108
108
  if (!routes.ok || !directory.ok) return [];
109
- const merged = new Map<string, { value: string; label: string; settingsNs: string; settingsPath: readonly string[] }>();
109
+ const merged = new Map<
110
+ string,
111
+ { value: string; label: string; settingsNs: string; settingsPath: readonly string[] }
112
+ >();
110
113
  for (const entry of directory.value) {
111
114
  merged.set(entry.provider, {
112
115
  value: entry.provider,
package/src/index.ts CHANGED
@@ -297,7 +297,10 @@ export class SessionModes extends Service {
297
297
 
298
298
  // 把当前挂着的 preset 落进模式的白名单:已经在名单里就返回 `undefined`(不换);不在里面(或读不到当前那份)
299
299
  // 就返回名单的**第一个**(`undefined` = 名单为空 = 不限制,什么都不换)。
300
- private presetTarget(allowed: readonly string[], current: string | undefined): string | undefined {
300
+ private presetTarget(
301
+ allowed: readonly string[],
302
+ current: string | undefined,
303
+ ): string | undefined {
301
304
  if (allowed.length === 0) return undefined;
302
305
  // 当前那份在名单里:保持不动(用户自己选的 preset 落在许可范围内,模式不去覆盖它)。
303
306
  if (current !== undefined && allowed.includes(current)) return undefined;
@@ -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。
@@ -83,10 +86,7 @@ export const MODE_SOURCES: readonly ModeSource[] = [
83
86
  // 官方 Office 技能的三个名字同样按会话排除:技能目录里不列它们,`skill` 工具加载它们也被拒。
84
87
  // 名单是技能名这一层的模型可见契约(官方 `skill-office` 的 `SKILL_NAMES`),跟着上游改名走。
85
88
  denySkills: ["office-docx", "office-pptx", "office-xlsx"],
86
- //
87
- // `denyPolicies` 只禁 `fs/edit-intent`(上游那条"先读后改"):改文件不再要求先读过——写路径上的
88
- // `fs/write-intent`(陈旧版本 CAS 那层安全网)照旧生效,那正是这条配置不写成"两条都禁"的理由。
89
- denyPolicies: ["fs/edit-intent"],
89
+ denyPolicies: ["fs/edit-intent", "fs/write-intent"],
90
90
  },
91
91
  {
92
92
  id: "chat",
@@ -103,9 +103,12 @@ export const MODE_SOURCES: readonly ModeSource[] = [
103
103
  // 配了只是噪音,所以留空。
104
104
  // 没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
105
105
  //
106
- // 两个抑制面都关:`instructions: false` 丢掉官方 `agent-instructions`(工作区指令)的注入;这个模式不写
106
+ // 注入面全关:`instructions: false` 丢掉官方 `agent-instructions`(工作区指令)的注入;这个模式不写
107
107
  // `skills`,而白名单三件里没有 `skill`、`denyTools` 也留空 → 推导成 `false`,官方 `skill-catalog` 的注入
108
- // 同样丢掉。取舍见 `.agents/designs/20260929-抑制官方注入面.md`。
108
+ // 同样丢掉;`runtimeContext: false` 连动态快照与时钟(`standard` 那类 preset 声明的 `time-context`)一起收。
109
+ // **收不掉的**:上游 `working-directory:current` 是 required 的目录上下文(0.2.1-alpha.2 起工作目录只走这条),
110
+ // 抑制可选运行时上下文不影响它——chat 会话的模型仍会看到工作目录那条快照。
111
+ // 取舍见 `.agents/designs/20260929-抑制官方注入面.md`。
109
112
  instructions: false,
110
113
  runtimeContext: false,
111
114
  },
package/src/modes.ts CHANGED
@@ -72,8 +72,9 @@ export interface SessionMode {
72
72
  // denyTools` 里含 `skill` 就要(`allowTools` 留空即"全部",所以只有 `denyTools` 能把它推成 `false`)。
73
73
  // `false` 表示这个模式不要技能目录:丢掉官方 `skill-catalog` 的注入(`skill` 工具的收窄仍归 `allowTools`)。
74
74
  readonly skills?: boolean;
75
- // 是否要 runtime context(文件沙箱策略、审批策略那两条动态快照)。缺省要;`false` 表示这个模式不要它们
76
- // ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
75
+ // 是否要动态快照(文件沙箱策略、审批策略那两条 snapshot section,以及 `standard` 那类 preset 声明的时钟
76
+ // `time-context` 注入)。缺省要;`false` 表示这个模式不要它们——对话模式没有文件与 shell 工具,"能改工作区
77
+ // 哪些文件、要不要走审批"对它全是噪音,它也不谈时间。
77
78
  readonly runtimeContext: boolean;
78
79
  // 这个模式的默认模型;省略就跟全局 `agent-default-model`。**可选**:没配的模式在页面上不出现在这一行
79
80
  // (`defaultModel` 是它所在模式的一个可加字段)。
@@ -277,13 +278,13 @@ const modeSchema: z<SessionMode> = z.object({
277
278
  .default(true)
278
279
  .description(
279
280
  localized({
280
- zh: "是否要动态快照(文件沙箱策略、审批策略)。",
281
- en: "Whether the runtime snapshot applies (sandbox and approval policy).",
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.",
282
283
  }),
283
284
  ),
284
285
  // 这个模式的默认模型。不标 `volatile`:`modes` 本身就是 volatile,整棵子树都在页面上——再标一层会被
285
286
  // schemastery 拒(`validateVolatileSchema` 不许 volatile 套 volatile)。
286
- defaultModel: modelSchema // `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
287
+ defaultModel: modelSchema // `default(null)` 是"没配就没有这个键":schemastery 对缺省的对象字段会造一个空对象,那样每个模式都会
287
288
  // 凭空多出一行;给了 null 反而让它保持缺失(页面按非必填处理,从候选加成)。
288
289
  // 类型上放行一次:`null` 在这里只是"没有这个键"的写法,schema 的输入形状不接受它。
289
290
  .default(null as unknown as SessionModeModel)
package/src/scope.ts CHANGED
@@ -6,11 +6,11 @@
6
6
  // - **装配期投影**:模型目录与 `tool:<工具名>` section 按同一份合成结果过滤;
7
7
  // - **执行层 guard**(在 `agent.ctx` 的 `tools` inject 回调里):拒绝时给该模式的文案,并说得出是哪一类(白名单外 /
8
8
  // 黑名单内);
9
- // - **两个抑制面**(`agent/pre-step`):官方 `agent-instructions` 与 `skill-catalog` 的注入按开关丢掉——官方那两行
10
- // 在装配投影之外自己往瀑布里 append,只能在瀑布上丢(不动注册表、也不动它们的行);
9
+ // - **三个抑制面**(`agent/pre-step`):官方 `agent-instructions`、`skill-catalog` 与 `time-context` 的注入按开关
10
+ // 丢掉——官方那几行在装配投影之外自己往瀑布里 append,只能在瀑布上丢(不动注册表、也不动它们的行);
11
11
  // - **另两个开关**(注入面那个 `skills` 在上面那条里):`runtimeContext: false` 抑制动态快照
12
- // (`systemPrompt.suppressRuntimeContext`),`instructions: false` 关掉通道自己的降级注入
13
- // (`ctx.contextAssembler.setInstructions`)。
12
+ // (`systemPrompt.suppressRuntimeContext`)与时钟那条注入(`agent/pre-step` 上的 `time-context`),
13
+ // `instructions: false` 关掉通道自己的降级注入(`ctx.contextAssembler.setInstructions`)。
14
14
  //
15
15
  // 同一个 agent 再 `apply` 就是换一份(旧 disposer 全部收回);不走 `tools.restrict()`(会触发 `tools/change`),
16
16
  // 也不建 preset 子树。
@@ -41,7 +41,7 @@ export interface SessionScopeDefinition {
41
41
  readonly instructions: boolean;
42
42
  // 这个会话要不要技能目录(`false` → 丢掉官方 `skill-catalog` 的注入)。
43
43
  readonly skills: boolean;
44
- // 这个会话要不要动态快照(`false` → 按 scope 抑制)。
44
+ // 这个会话要不要动态快照(`false` → 按 scope 抑制,并在瀑布上丢掉时钟那条注入)。
45
45
  readonly runtimeContext: boolean;
46
46
  }
47
47
 
@@ -164,19 +164,15 @@ function skillNameOf(arguments_: unknown): string | undefined {
164
164
  return typeof name === "string" && name.length > 0 ? name : undefined;
165
165
  }
166
166
 
167
- // 一个 agent 当前装着的那一份:`apply` 时解析好的开关(抑制面读它)。
167
+ // 一个 agent 当前装着的那一份:装配期过滤读它(`apply` 时解析好的名单)。
168
168
  interface Applied {
169
169
  // 两条名单都空 = 不过滤(装配期与执行层都放行)。
170
170
  readonly gate: NameGate | undefined;
171
- // 技能面的名单:与工具面同形,`undefined` = 不设收窄(目录照旧、`skill` 工具不按名判)。
172
- readonly skillGate: NameGate | undefined;
173
- readonly instructions: boolean;
174
- readonly skills: boolean;
175
171
  // 收回这一份在 `agent.ctx` 上的注册(抑制器与执行层 guard)。
176
172
  readonly dispose: () => void;
177
173
  }
178
174
 
179
- // 按会话收口工具面与注入面的状态机:一份 per-agent 的状态 + 两条全局监听器。
175
+ // 按会话收口工具面与注入面的状态机:一份 per-agent 的装配期名单 + 挂在各 agent 上的注册。
180
176
  export class SessionScope {
181
177
  private readonly applied = new WeakMap<Agent, Applied>();
182
178
  private readonly ctx: Context;
@@ -201,54 +197,6 @@ export class SessionScope {
201
197
  tools: result.tools.filter((tool) => denialOf(gate, tool.name) === undefined),
202
198
  };
203
199
  });
204
-
205
- // 两个抑制面:官方 `agent-instructions`(工作区指令)与官方 `skill-catalog`(技能目录)都在装配投影之外自己
206
- // 往这条瀑布里 append,所以只有在这里丢掉它们的条目(技能名单则在这里把目录按名收窄)。
207
- //
208
- // `prepend` 是必需的:官方那两行比本行早注册(会话挂着的 preset 先于 host 平面),瀑布里先注册的是**外层**,
209
- // 站在它们后面就看不到、也丢不掉它们 append 的条目——只有抢在最外层(`await next()` 之后再收)才拿得到最终
210
- // 消息表。
211
- ctx.on(
212
- "agent/pre-step",
213
- async (payload, next): Promise<PreStepDecision> => {
214
- const decision = await next();
215
- if (decision.kind !== "enter") return decision;
216
- const applied = this.applied.get(payload.agent);
217
- // 没收过口的会话一律不动。
218
- if (applied === undefined) return decision;
219
- let changed = false;
220
- const kept: UserMessage[] = [];
221
- for (const message of decision.messages) {
222
- const kind = kindOf(message);
223
- if (kind === "agent-instructions") {
224
- if (applied.instructions) kept.push(message);
225
- else changed = true;
226
- continue;
227
- }
228
- if (kind === "skill-catalog") {
229
- if (!applied.skills) {
230
- changed = true;
231
- continue;
232
- }
233
- const narrowed =
234
- applied.skillGate === undefined
235
- ? undefined
236
- : narrowCatalog(message, applied.skillGate, (line) =>
237
- this.ctx.logger.warn(`session-mode: ${line}`),
238
- );
239
- if (narrowed === undefined) kept.push(message);
240
- else {
241
- changed = true;
242
- kept.push(narrowed);
243
- }
244
- continue;
245
- }
246
- kept.push(message);
247
- }
248
- return changed ? { ...decision, messages: kept } : decision;
249
- },
250
- { prepend: true },
251
- );
252
200
  }
253
201
 
254
202
  // 把某个会话收口到这份定义上(同一个 agent 再调就是换一份)。
@@ -259,10 +207,29 @@ export class SessionScope {
259
207
  const gate = gateOfNames(definition.allowTools, definition.denyTools);
260
208
  const skillGate = gateOfNames(definition.allowSkills, definition.denySkills);
261
209
 
262
- // 落在 `agent.ctx` 上的那两件用一个 effect 装:动态快照抑制是 scope 层的一次注册;执行层 guard 要等
263
- // `tools` 激活才装得上(`inject` 的回调),它挂在那个 inject fiber 下,所以收回时连 fiber 一起收。
210
+ // 落在 `agent.ctx` 上的那几件用一个 effect 装:注入面的抑制与动态快照抑制是 scope 层的注册;执行层 guard
211
+ // 要等 `tools` 激活才装得上(`inject` 的回调),它挂在那个 inject fiber 下,所以收回时连 fiber 一起收。
264
212
  const dispose = agent.ctx.effect(() => {
265
213
  const stoppers: (() => void)[] = [];
214
+ // 三个抑制面:官方 `agent-instructions`(工作区指令)、官方 `skill-catalog`(技能目录)与官方 `time-context`
215
+ // (时钟)都在装配投影之外自己往这条瀑布里 append,所以只能在这里丢它们的条目(技能目录那条还要按名收窄)。
216
+ //
217
+ // 两件事让这条注册必须落在**会话**上而不是行上:
218
+ // - `prepend` 只是把自己排到表头,不保证一直在那儿:表头归**最后**注册的那个 prepend,而时钟那条也 prepend;
219
+ // - 官方那几行随 preset 挂载(早于 `agent/created`,本注册就在那儿),所以按会话注册天然排在它们之后。
220
+ // 换模式 / 换 preset 时 `apply` 会重来一遍(旧的收回、新的 prepend),最外层由此回到我们手上。
221
+ stoppers.push(
222
+ agent.ctx.on(
223
+ "agent/pre-step",
224
+ async (_payload, next): Promise<PreStepDecision> => {
225
+ const decision = await next();
226
+ if (decision.kind !== "enter") return decision;
227
+ const kept = this.suppress(decision.messages, definition, skillGate);
228
+ return kept === undefined ? decision : { ...decision, messages: kept };
229
+ },
230
+ { prepend: true },
231
+ ),
232
+ );
266
233
  if (definition.runtimeContext === false) {
267
234
  // service 在 `agent.ctx` 上是 shadow:抑制落在该 agent 的 scope 层,只覆盖这个会话。
268
235
  stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
@@ -292,14 +259,56 @@ export class SessionScope {
292
259
 
293
260
  this.applied.set(agent, {
294
261
  gate,
295
- skillGate,
296
- instructions: definition.instructions,
297
- skills: definition.skills,
298
262
  dispose,
299
263
  });
300
264
  }
301
265
 
302
- // 通道是可选的搭档:没有它就没有降级注入可关,工具面与两个抑制面照常生效。
266
+ // 三个抑制面按这份定义过一遍本步的消息表:`undefined` = 一条都不动(开关都是"要"、技能名单也没排除谁),
267
+ // 其余 = 过滤 / 按名收窄之后的那份表。
268
+ private suppress(
269
+ messages: readonly UserMessage[],
270
+ definition: SessionScopeDefinition,
271
+ skillGate: NameGate | undefined,
272
+ ): UserMessage[] | undefined {
273
+ let changed = false;
274
+ const kept: UserMessage[] = [];
275
+ for (const message of messages) {
276
+ const kind = kindOf(message);
277
+ if (kind === "agent-instructions") {
278
+ if (definition.instructions) kept.push(message);
279
+ else changed = true;
280
+ continue;
281
+ }
282
+ if (kind === "skill-catalog") {
283
+ if (!definition.skills) {
284
+ changed = true;
285
+ continue;
286
+ }
287
+ const narrowed =
288
+ skillGate === undefined
289
+ ? undefined
290
+ : narrowCatalog(message, skillGate, (line) =>
291
+ this.ctx.logger.warn(`session-mode: ${line}`),
292
+ );
293
+ if (narrowed === undefined) kept.push(message);
294
+ else {
295
+ changed = true;
296
+ kept.push(narrowed);
297
+ }
298
+ continue;
299
+ }
300
+ if (kind === "time-context") {
301
+ // 时钟也归"动态快照":它不经 `suppressRuntimeContext`(那是 snapshot section 那条路),只在瀑布上丢。
302
+ if (definition.runtimeContext) kept.push(message);
303
+ else changed = true;
304
+ continue;
305
+ }
306
+ kept.push(message);
307
+ }
308
+ return changed ? kept : undefined;
309
+ }
310
+
311
+ // 通道是可选的搭档:没有它就没有降级注入可关,工具面与三个抑制面照常生效。
303
312
  private channel(): { setInstructions(agent: Agent, on: boolean): void } | undefined {
304
313
  try {
305
314
  return this.ctx.get("contextAssembler");
package/src/testing.ts ADDED
@@ -0,0 +1,3 @@
1
+ // 本包测试辅助:client 面的测试要用宿主面的目录服务与它的远程面常量(跨面的引用走出口,不走相对路径)。
2
+ export { SessionModeCatalog } from "./catalog.ts";
3
+ export { CATALOG_NS, SESSION_MODE_CATALOG_REMOTE } from "./catalog-remote.ts";
package/dist/index.d.cts DELETED
@@ -1,146 +0,0 @@
1
- import { Context, Service, Volatile } from "@deepseek-ai/cordis";
2
- import { Agent } from "@deepseek-ai/dsh-agent";
3
- import { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
4
- import { z } from "zod";
5
- import z$1 from "@deepseek-ai/schemastery";
6
- //#region src/modes.d.ts
7
- interface SessionModePersona {
8
- readonly prefix: string;
9
- readonly suffix: string;
10
- }
11
- type SessionModeRole = "main" | "subagent";
12
- interface SessionModeModel {
13
- readonly provider: string;
14
- readonly model: string;
15
- readonly reasoningEffort?: string;
16
- }
17
- type SessionModeModels = Readonly<Record<string, SessionModeModel>>;
18
- interface SessionMode {
19
- readonly presetsOnly: string[];
20
- readonly name: string;
21
- readonly description: string;
22
- readonly role: SessionModeRole[];
23
- readonly persona: SessionModePersona;
24
- readonly allowTools: string[];
25
- readonly denyTools: string[];
26
- readonly allowSkills: string[];
27
- readonly denySkills: string[];
28
- readonly allowPolicies: string[];
29
- readonly denyPolicies: string[];
30
- readonly instructions: boolean;
31
- readonly skills?: boolean;
32
- readonly runtimeContext: boolean;
33
- readonly defaultModel?: SessionModeModel;
34
- readonly preset?: string;
35
- }
36
- export interface Config {
37
- readonly default: string;
38
- readonly modes: Record<string, SessionMode>;
39
- readonly models?: SessionModeModels;
40
- }
41
- interface ResolvedConfig {
42
- readonly default: Volatile<string>;
43
- readonly modes: Volatile<Record<string, SessionMode>>;
44
- readonly models: SessionModeModels;
45
- }
46
- export declare const Config: z$1<Config, ResolvedConfig>;
47
- //#endregion
48
- //#region src/shared.d.ts
49
- export declare const SESSION_MODE_PATH = "/session-mode";
50
- export declare const POLICY_NAMES: readonly ["fs/write-intent", "fs/edit-intent"];
51
- type PolicyName = (typeof POLICY_NAMES)[number];
52
- interface SessionModeRow {
53
- readonly id: string;
54
- readonly name: string;
55
- readonly description?: string;
56
- }
57
- interface SessionModeRoster {
58
- readonly default: string;
59
- readonly modes: readonly SessionModeRow[];
60
- }
61
- //#endregion
62
- //#region src/index.d.ts
63
- export declare const name = "session-mode";
64
- export declare const inject: string[];
65
- declare module "@deepseek-ai/cordis" {
66
- interface Context {
67
- sessionModes: SessionModes;
68
- }
69
- }
70
- declare module "@deepseek-ai/dsh-session/types" {
71
- interface SessionEventMap {
72
- "session-mode/selected": {
73
- sessionMode: string;
74
- };
75
- }
76
- }
77
- declare module "@deepseek-ai/dsh-session-projection/types" {
78
- interface SessionProjectionStateMap {
79
- sessionMode: string | null;
80
- sessionModeEditable: boolean;
81
- }
82
- interface SessionProjectionMap {
83
- sessionMode: string | null;
84
- sessionModeEditable: boolean;
85
- }
86
- }
87
- export declare const sessionModeProjection: {
88
- key: "sessionMode";
89
- stateSchema: z.ZodType<string | null, unknown, z.core.$ZodTypeInternals<string | null, unknown>>;
90
- init: () => null;
91
- apply: (state: string | null, event: SessionEvent) => string | null;
92
- wire: {
93
- viewSchema: z.ZodType<string | null, unknown, z.core.$ZodTypeInternals<string | null, unknown>>;
94
- view: (state: string | null) => string | null;
95
- };
96
- stateVersion: number;
97
- };
98
- export declare const sessionModeEditableProjection: {
99
- key: "sessionModeEditable";
100
- stateSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
101
- init: () => true;
102
- apply: (state: boolean, event: SessionEvent) => boolean;
103
- wire: {
104
- viewSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
105
- view: (state: boolean) => boolean;
106
- };
107
- stateVersion: number;
108
- };
109
- export declare class SessionModes extends Service {
110
- private readonly installs;
111
- private readonly scope;
112
- readonly config: {
113
- default: string;
114
- modes: Record<string, SessionMode>;
115
- };
116
- constructor(ctx: Context, config: ResolvedConfig);
117
- get defaultId(): string;
118
- list(): SessionModeRow[];
119
- idsFor(role: SessionModeRole): string[];
120
- modesFor(role: SessionModeRole): {
121
- id: string;
122
- mode: SessionMode;
123
- }[];
124
- roster(): SessionModeRoster;
125
- definition(id?: string): SessionMode;
126
- modeOf(session: Session): string;
127
- modeOfSession(session: Session): SessionMode;
128
- private policyInForce;
129
- private decidePolicy;
130
- select(sessionId: SessionId, mode: string): Promise<string>;
131
- private presetTarget;
132
- private presetOfAgent;
133
- private presetRegistry;
134
- applyTo(agent: Agent, mode: string, options?: {
135
- record?: boolean;
136
- }): void;
137
- private resolveModeId;
138
- private presetOf;
139
- modeForPreset(preset: string | undefined): string | undefined;
140
- private inheritedModeId;
141
- private installFor;
142
- private installDefaultModel;
143
- }
144
- export declare function apply(ctx: Context, config: ResolvedConfig): void;
145
- //#endregion
146
- export type { PolicyName, ResolvedConfig, SessionMode, SessionModeModel, SessionModePersona, SessionModeRole, SessionModeRoster, SessionModeRow };
package/dist/rows.d.cts DELETED
@@ -1,11 +0,0 @@
1
- //#region src/rows.d.ts
2
- export interface PatchRow {
3
- readonly insert?: readonly RowEntry[];
4
- }
5
- export interface RowEntry {
6
- readonly id: string;
7
- readonly name: string;
8
- readonly config?: Readonly<Record<string, unknown>>;
9
- }
10
- export declare function sessionModeRows(): readonly PatchRow[];
11
- //#endregion