@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/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,144 +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 preset: 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
- }
35
- export interface Config {
36
- readonly default: string;
37
- readonly modes: Record<string, SessionMode>;
38
- readonly models?: SessionModeModels;
39
- }
40
- interface ResolvedConfig {
41
- readonly default: Volatile<string>;
42
- readonly modes: Volatile<Record<string, SessionMode>>;
43
- readonly models: SessionModeModels;
44
- }
45
- export declare const Config: z$1<Config, ResolvedConfig>;
46
- //#endregion
47
- //#region src/shared.d.ts
48
- export declare const SESSION_MODE_PATH = "/session-mode";
49
- export declare const POLICY_NAMES: readonly ["fs/write-intent", "fs/edit-intent"];
50
- type PolicyName = (typeof POLICY_NAMES)[number];
51
- interface SessionModeRow {
52
- readonly id: string;
53
- readonly name: string;
54
- readonly description?: string;
55
- }
56
- interface SessionModeRoster {
57
- readonly default: string;
58
- readonly modes: readonly SessionModeRow[];
59
- }
60
- //#endregion
61
- //#region src/index.d.ts
62
- export declare const name = "session-mode";
63
- export declare const inject: string[];
64
- declare module "@deepseek-ai/cordis" {
65
- interface Context {
66
- sessionModes: SessionModes;
67
- }
68
- }
69
- declare module "@deepseek-ai/dsh-session/types" {
70
- interface SessionEventMap {
71
- "session-mode/selected": {
72
- sessionMode: string;
73
- };
74
- }
75
- }
76
- declare module "@deepseek-ai/dsh-session-projection/types" {
77
- interface SessionProjectionStateMap {
78
- sessionMode: string | null;
79
- sessionModeEditable: boolean;
80
- }
81
- interface SessionProjectionMap {
82
- sessionMode: string | null;
83
- sessionModeEditable: boolean;
84
- }
85
- }
86
- export declare const sessionModeProjection: {
87
- key: "sessionMode";
88
- stateSchema: z.ZodType<string | null, unknown, z.core.$ZodTypeInternals<string | null, unknown>>;
89
- init: () => null;
90
- apply: (state: string | null, event: SessionEvent) => string | null;
91
- wire: {
92
- viewSchema: z.ZodType<string | null, unknown, z.core.$ZodTypeInternals<string | null, unknown>>;
93
- view: (state: string | null) => string | null;
94
- };
95
- stateVersion: number;
96
- };
97
- export declare const sessionModeEditableProjection: {
98
- key: "sessionModeEditable";
99
- stateSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
100
- init: () => true;
101
- apply: (state: boolean, event: SessionEvent) => boolean;
102
- wire: {
103
- viewSchema: z.ZodType<boolean, unknown, z.core.$ZodTypeInternals<boolean, unknown>>;
104
- view: (state: boolean) => boolean;
105
- };
106
- stateVersion: number;
107
- };
108
- export declare class SessionModes extends Service {
109
- private readonly installs;
110
- private readonly scope;
111
- readonly config: {
112
- default: string;
113
- modes: Record<string, SessionMode>;
114
- };
115
- constructor(ctx: Context, config: ResolvedConfig);
116
- get defaultId(): string;
117
- list(): SessionModeRow[];
118
- idsFor(role: SessionModeRole): string[];
119
- modesFor(role: SessionModeRole): {
120
- id: string;
121
- mode: SessionMode;
122
- }[];
123
- roster(): SessionModeRoster;
124
- definition(id?: string): SessionMode;
125
- modeOf(session: Session): string;
126
- modeOfSession(session: Session): SessionMode;
127
- private policyInForce;
128
- private decidePolicy;
129
- select(sessionId: SessionId, mode: string): Promise<string>;
130
- private presetOfAgent;
131
- private presetRegistry;
132
- applyTo(agent: Agent, mode: string, options?: {
133
- record?: boolean;
134
- }): void;
135
- private resolveModeId;
136
- private presetOf;
137
- modeForPreset(preset: string | undefined): string | undefined;
138
- private inheritedModeId;
139
- private installFor;
140
- private installDefaultModel;
141
- }
142
- export declare function apply(ctx: Context, config: ResolvedConfig): void;
143
- //#endregion
144
- 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
@@ -1,74 +0,0 @@
1
- /* 新会话屏幕上的模式 chip,紧邻工作区占位:几何与 HeroShell 的 .workspace 对齐,两个读起来是一行。
2
- 标签以省略号收窄,图标与箭头始终贴住剩余文本。 */
3
-
4
- .menuAnchor {
5
- min-width: 54px;
6
- max-width: 100%;
7
- }
8
-
9
- .seat {
10
- display: inline-flex;
11
- align-items: center;
12
- gap: 4px;
13
- min-width: 0;
14
- max-width: min(100%, 240px);
15
- min-height: 28px;
16
- padding: 0 8px;
17
- border: none;
18
- border-radius: 16px;
19
- background: transparent;
20
- color: var(--dsw-alias-label-primary);
21
- font-size: 13px;
22
- line-height: 20px;
23
- font-weight: 500;
24
- white-space: nowrap;
25
- cursor: pointer;
26
- }
27
-
28
- .seat:not(:disabled):hover,
29
- .seat[aria-expanded="true"] {
30
- background: var(--dsw-alias-interactive-bg-hover);
31
- }
32
-
33
- .seat:disabled {
34
- cursor: default;
35
- color: var(--dsw-alias-label-quaternary);
36
- }
37
-
38
- .seatIcon {
39
- flex: none;
40
- color: var(--dsw-alias-label-primary);
41
- }
42
-
43
- .seatLabel {
44
- min-width: 0;
45
- overflow: hidden;
46
- text-overflow: ellipsis;
47
- white-space: nowrap;
48
- }
49
-
50
- .chevron {
51
- flex: none;
52
- color: var(--dsw-alias-label-caption);
53
- }
54
-
55
- /* 菜单行是"名字 + 说明":光有 id 说不清这个模式干什么。 */
56
- .item {
57
- display: flex;
58
- flex-direction: column;
59
- gap: 2px;
60
- max-width: 280px;
61
- }
62
-
63
- .itemName {
64
- font-size: 13px;
65
- line-height: 20px;
66
- color: var(--dsw-alias-label-primary);
67
- }
68
-
69
- .itemDesc {
70
- font-size: 12px;
71
- line-height: 16px;
72
- color: var(--dsw-alias-label-caption);
73
- white-space: normal;
74
- }