@morlay/dsh-session-mode 0.0.12 → 0.0.14-alpha.0
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/dist/{catalog-K5cESbqA.cjs → catalog-C_6kb1aC.cjs} +2 -3
- package/dist/{catalog-DWxIg3qw.mjs → catalog-D-n_RctY.mjs} +2 -3
- package/dist/{catalog-remote-KXJjDaN6.mjs → catalog-remote-Cz8L_Zrl.mjs} +1 -2
- package/dist/{catalog-remote-B7ZgYqun.cjs → catalog-remote-DWV4E4Vo.cjs} +1 -2
- package/dist/client.cjs +1263 -555
- package/dist/client.d.cts +0 -14
- package/dist/client.d.mts +0 -14
- package/dist/client.mjs +11 -22
- package/dist/index.cjs +62 -42
- package/dist/index.d.mts +9 -7
- package/dist/index.mjs +62 -42
- package/dist/rows.cjs +8 -6
- package/dist/rows.d.mts +1 -3
- package/dist/rows.mjs +8 -6
- package/dist/{shared-BusmswG5.cjs → shared-DO1nZbJ0.cjs} +2 -4
- package/dist/{shared-DX2Giyh3.mjs → shared-vjlwxiMt.mjs} +1 -2
- package/dist/testing.cjs +2 -2
- package/dist/testing.d.mts +0 -4
- package/dist/testing.mjs +2 -2
- package/package.json +58 -58
- package/src/client/index.ts +1 -1
- package/src/index.ts +62 -21
- package/src/mode-sources.ts +2 -1
- package/src/scope.ts +53 -36
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// `modes.<id>.defaultModel`;换模式只允许在空白窗口。取舍见 `.agents/designs/20260924-会话模式.md`。
|
|
4
4
|
|
|
5
5
|
import { Service, type Context } from "@deepseek-ai/cordis";
|
|
6
|
+
import { modelSelectionProjectionDefinition } from "@deepseek-ai/dsh-agent";
|
|
6
7
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
7
8
|
// 会话级模型事实(投影 `modelSelection` 与事件 `model/selection`)由上游 session-controller 声明;
|
|
8
9
|
// 那一行可能没装(headless 部署),所以读它时按"可能为空"处理。
|
|
@@ -10,11 +11,10 @@ import type {} from "@deepseek-ai/dsh-api-session-controller";
|
|
|
10
11
|
import { ReasoningEffortId, type LlmCallConfig } from "@deepseek-ai/dsh-llm";
|
|
11
12
|
import type { Session, SessionEvent, SessionId } from "@deepseek-ai/dsh-session";
|
|
12
13
|
import type {} from "@deepseek-ai/dsh-session";
|
|
13
|
-
import
|
|
14
|
+
import { agentPresetProjectionDefinition } from "@deepseek-ai/dsh-agent-preset-registry";
|
|
15
|
+
import type { ProjectionDefinition, ProjectionHandle } from "@deepseek-ai/dsh-session-projection";
|
|
14
16
|
import type {} from "@deepseek-ai/dsh-session-projection";
|
|
15
|
-
//
|
|
16
|
-
// 我们读它的选择、落成自己的会话事实。
|
|
17
|
-
import type {} from "@deepseek-ai/dsh-agent-preset-registry";
|
|
17
|
+
// 官方 roster 的会话投影(`agentPreset`):选择面归官方 preset,我们读它的选择、落成自己的会话事实。
|
|
18
18
|
// Type-only:上游 fs 的 policy 事件词汇(`fs/write-intent` / `fs/edit-intent` 的签名与 `FsWriteIntent`)。
|
|
19
19
|
import type {} from "@deepseek-ai/dsh-fs";
|
|
20
20
|
import { z } from "zod";
|
|
@@ -116,6 +116,14 @@ export class SessionModes extends Service {
|
|
|
116
116
|
// 输入就是模式定义、唯一消费者也是模式。
|
|
117
117
|
private readonly scope: SessionScope;
|
|
118
118
|
|
|
119
|
+
// 会话投影的读句柄(上游 0.2.1-rc.2 起 `register()` 是异步的,读状态要拿着那次注册的 handle;
|
|
120
|
+
// 全局的 `stateOf(session, key)` 已弃用)。前两个是本包自己的投影,后两个是上游的(同一份定义重复注册
|
|
121
|
+
// 只是共享同一个 unit,不改变它的行为)。
|
|
122
|
+
private projectionHandle!: ProjectionHandle<string | null>;
|
|
123
|
+
private editableHandle!: ProjectionHandle<boolean>;
|
|
124
|
+
private modelSelectionHandle!: ProjectionHandle<{ readonly pending: unknown; readonly lastUsed: unknown }>;
|
|
125
|
+
private agentPresetHandle!: ProjectionHandle<string | null>;
|
|
126
|
+
|
|
119
127
|
// 装配时的配置快照:`default` / `modes` 读它,改这两项靠 Loader 重挂这一行(已运行会话不自动换定义)。
|
|
120
128
|
readonly config: {
|
|
121
129
|
default: string;
|
|
@@ -134,8 +142,6 @@ export class SessionModes extends Service {
|
|
|
134
142
|
const problem = configProblem({ ...this.config, models: config.models });
|
|
135
143
|
if (problem !== undefined) throw new Error(problem);
|
|
136
144
|
this.scope = new SessionScope(ctx);
|
|
137
|
-
ctx.sessionProjections.register(sessionModeProjection);
|
|
138
|
-
ctx.sessionProjections.register(sessionModeEditableProjection);
|
|
139
145
|
// 按模式的 policy 拦截:两条上游 waterfall 各 `prepend` **一次**,注册在行 ctx 上、随行卸载一起撤。
|
|
140
146
|
//
|
|
141
147
|
// 接缝为什么只有这一种:fs 的调用是 `ctx.waterfall('fs/write-intent', target, actor, next)`——事件名在第一个
|
|
@@ -169,13 +175,35 @@ export class SessionModes extends Service {
|
|
|
169
175
|
const mapped = this.modeForPreset(preset);
|
|
170
176
|
const agent = ctx.agents.get(sessionId);
|
|
171
177
|
if (mapped === undefined || agent === undefined) return;
|
|
172
|
-
if (this.
|
|
178
|
+
if (this.projectionHandle.stateOf(agent.session) !== mapped) {
|
|
173
179
|
agent.session.append("session-mode/selected", { sessionMode: mapped });
|
|
174
180
|
}
|
|
175
181
|
this.installFor(agent, mapped);
|
|
176
182
|
});
|
|
177
183
|
}
|
|
178
184
|
|
|
185
|
+
// 构造即注册(`init` 是异步的):入口 `apply` 走这条异步工厂。
|
|
186
|
+
static async create(ctx: Context, config: ResolvedConfig): Promise<SessionModes> {
|
|
187
|
+
const modes = new SessionModes(ctx, config);
|
|
188
|
+
await modes.init();
|
|
189
|
+
return modes;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 注册投影并拿读句柄。`register()` 要 await:句柄与注册同生命期(行卸载时自动释放),而读状态只能经句柄。
|
|
193
|
+
private async init(): Promise<void> {
|
|
194
|
+
this.projectionHandle = await this.ctx.sessionProjections.register(sessionModeProjection);
|
|
195
|
+
this.editableHandle = await this.ctx.sessionProjections.register(sessionModeEditableProjection);
|
|
196
|
+
// 上游那份 `modelSelection` 由 agent-loop 注册;这里拿同一个定义再注册一次只是要一个读句柄
|
|
197
|
+
// (refs 共享,卸载时我们这一份跟着减回去)。headless 部署里 agent-loop 也在,所以 key 一定有人注册。
|
|
198
|
+
this.modelSelectionHandle = await this.ctx.sessionProjections.register(
|
|
199
|
+
modelSelectionProjectionDefinition,
|
|
200
|
+
);
|
|
201
|
+
// `agentPreset` 归官方 roster;同样只借它的定义读会话事实(registry 没装时 header 里的值照旧可读)。
|
|
202
|
+
this.agentPresetHandle = await this.ctx.sessionProjections.register(
|
|
203
|
+
agentPresetProjectionDefinition,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
179
207
|
// 新会话用它:config 里的 `default`。
|
|
180
208
|
get defaultId(): string {
|
|
181
209
|
return this.config.default;
|
|
@@ -222,12 +250,23 @@ export class SessionModes extends Service {
|
|
|
222
250
|
return mode;
|
|
223
251
|
}
|
|
224
252
|
|
|
253
|
+
// 会话自己选过的模式(投影原值;没选过是 null)。`modeOf` 在它之上落部署默认。
|
|
254
|
+
selectedOf(session: Session): string | null {
|
|
255
|
+
return this.projectionHandle.stateOf(session);
|
|
256
|
+
}
|
|
257
|
+
|
|
225
258
|
// 会话当前模式:投影上有就用它,否则是部署默认。
|
|
226
259
|
modeOf(session: Session): string {
|
|
227
|
-
const selected = this.
|
|
260
|
+
const selected = this.projectionHandle.stateOf(session);
|
|
228
261
|
return selected ?? this.defaultId;
|
|
229
262
|
}
|
|
230
263
|
|
|
264
|
+
// 这个会话还能不能换模式(空白窗口):投影 `sessionModeEditable` 的读口。
|
|
265
|
+
// 页面 chip 的只读形态与服务端的拒绝判据都读它。
|
|
266
|
+
editableOf(session: Session): boolean {
|
|
267
|
+
return this.editableHandle.stateOf(session);
|
|
268
|
+
}
|
|
269
|
+
|
|
231
270
|
// 会话当前模式的定义。
|
|
232
271
|
modeOfSession(session: Session): SessionMode {
|
|
233
272
|
return this.definition(this.modeOf(session));
|
|
@@ -274,7 +313,7 @@ export class SessionModes extends Service {
|
|
|
274
313
|
const session = this.ctx.sessions.get(sessionId);
|
|
275
314
|
if (session === undefined) throw new Error(`未知的会话 ${sessionId}`);
|
|
276
315
|
// 空白窗口的判据只有一处:`sessionModeEditableProjection`(client chip 读同一个投影)。
|
|
277
|
-
if (this.
|
|
316
|
+
if (this.editableHandle.stateOf(session) === false) {
|
|
278
317
|
throw new Error("这个会话已经开始,模式不能再改;要换模式请新开一个会话。");
|
|
279
318
|
}
|
|
280
319
|
const agent = this.ctx.agents.get(sessionId);
|
|
@@ -287,7 +326,7 @@ export class SessionModes extends Service {
|
|
|
287
326
|
await registry.select(agent, target);
|
|
288
327
|
}
|
|
289
328
|
}
|
|
290
|
-
if (this.
|
|
329
|
+
if (this.projectionHandle.stateOf(session) !== mode) {
|
|
291
330
|
session.append("session-mode/selected", { sessionMode: mode });
|
|
292
331
|
}
|
|
293
332
|
// 会话可能已经建好了 agent(空白会话也有):立刻按新模式重新应用一遍(与监听那次重复也无妨,幂等)。
|
|
@@ -344,7 +383,7 @@ export class SessionModes extends Service {
|
|
|
344
383
|
// 继承要写进子会话日志——它是一条会话事实,冷恢复与 fork 靠它重建(`modeOf` 只读投影)。
|
|
345
384
|
private resolveModeId(agent: Agent): string {
|
|
346
385
|
// `undefined` = 这个投影没注册(或还没初始化),与"没选过"(`null`)一样落到下一层。
|
|
347
|
-
const selected = this.
|
|
386
|
+
const selected = this.projectionHandle.stateOf(agent.session);
|
|
348
387
|
if (typeof selected === "string") return selected;
|
|
349
388
|
// 会话级选择归官方 roster;结论落成我们自己的会话事实(`sessionMode`:恢复、子代理继承、
|
|
350
389
|
// 服务端读取都读它)。
|
|
@@ -361,13 +400,9 @@ export class SessionModes extends Service {
|
|
|
361
400
|
|
|
362
401
|
// 官方 roster 选定的 preset(没选过、或 registry 没装时为 `undefined`)。
|
|
363
402
|
private presetOf(session: Session): string | undefined {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
} catch {
|
|
368
|
-
// registry 没装(headless 部署)时这个投影不存在:按"没有选择"处理。
|
|
369
|
-
return undefined;
|
|
370
|
-
}
|
|
403
|
+
// registry 没装(headless 部署)时 header 里没有 preset,读到的就是 `null`:与"没选过"同义。
|
|
404
|
+
const state = this.agentPresetHandle.stateOf(session);
|
|
405
|
+
return typeof state === "string" ? state : undefined;
|
|
371
406
|
}
|
|
372
407
|
|
|
373
408
|
// 某个 preset 对应的模式 id——只在映射唯一时回答(只有它一个模式的名单里有这份 preset;没有模式认领、或几个
|
|
@@ -420,13 +455,19 @@ export class SessionModes extends Service {
|
|
|
420
455
|
|
|
421
456
|
// 模式的默认模型兜底:只在会话尚无任何模型事实(没选过模型、也没落过 request header)时接管这一请求的
|
|
422
457
|
// 路由。它是配置事实、不写会话事件,读构造时的模式快照(设置页保存会让这一行重挂)。
|
|
458
|
+
//
|
|
459
|
+
// `agent/request` 已被上游标为 deprecated("Retained for existing listeners; do not add new listeners",且明说
|
|
460
|
+
// 其他 per-request 控制**没有替代 API**)。这里是这个产品行为唯一的接缝:`agent.model.select()` 会写
|
|
461
|
+
// `model/selection` 事件,把"配置默认"变成"用户选择",直接破坏"只在没有模型事实时兜底"的语义
|
|
462
|
+
// (见 `../.agents/adrs/20260925-默认模型住在模式定义里.md`)。债见
|
|
463
|
+
// `.agents/debts/20261026-默认模型兜底依赖deprecated的agent-request.md`。
|
|
423
464
|
private installDefaultModel(agent: Agent, modeId: string): () => void {
|
|
424
465
|
return agent.ctx.on("agent/request", async (_payload, next): Promise<LlmCallConfig> => {
|
|
425
466
|
const resolved = await next();
|
|
426
467
|
const model = this.config.modes[modeId]?.defaultModel;
|
|
427
468
|
if (model === undefined) return resolved;
|
|
428
469
|
// 上游 session-controller 没装(headless)时这个投影不存在,按"没有选择"处理。
|
|
429
|
-
const pending = this.
|
|
470
|
+
const pending = this.modelSelectionHandle.stateOf(agent.session).pending;
|
|
430
471
|
if (pending !== undefined && pending !== null) return resolved;
|
|
431
472
|
if (agent.session.requestHeader() !== undefined) return resolved;
|
|
432
473
|
return {
|
|
@@ -441,8 +482,8 @@ export class SessionModes extends Service {
|
|
|
441
482
|
}
|
|
442
483
|
}
|
|
443
484
|
|
|
444
|
-
export function apply(ctx: Context, config: ResolvedConfig): void {
|
|
445
|
-
const modes =
|
|
485
|
+
export async function apply(ctx: Context, config: ResolvedConfig): Promise<void> {
|
|
486
|
+
const modes = await SessionModes.create(ctx, config);
|
|
446
487
|
registerHttpRoutes(ctx, modes);
|
|
447
488
|
// 配置页的工具 / 技能候选:一条本包自己开的 Remote 面(客户端半自己 mount,见 `./catalog-remote.ts`)。
|
|
448
489
|
ctx.plugin(SessionModeCatalog);
|
package/src/mode-sources.ts
CHANGED
|
@@ -53,6 +53,7 @@ const CODING_PERSONA = {
|
|
|
53
53
|
prefix: [
|
|
54
54
|
"你是一个经验丰富的编程专家,YAGNI 是你的编程哲学,PDCA 是你的行为规范。",
|
|
55
55
|
"全程 MUST 用中文(专有名词除外),包括但不限于思考,回答,工具描述,subagent 提示词;思考 NEVER 陷入重复循环,一旦循环立即退出;思考 SHOULD 聚焦需求理解与方案设计,不预演具体代码实现,正确性由验证环节确认。",
|
|
56
|
+
"单轮次内 MUST 完成明确的任务,任务未完成 NEVER 停下,除非有明确分歧点需要停下询问用户来做决断",
|
|
56
57
|
].join("\n"),
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -83,7 +84,7 @@ export const MODE_SOURCES: readonly ModeSource[] = [
|
|
|
83
84
|
role: ["main", "subagent"],
|
|
84
85
|
// 不写 `allowTools`:不设收窄——这个会话用它挂着的 preset 提供的全部工具(抄一份清单只会与行清单漂移)。
|
|
85
86
|
// 只排除一件工具:官方 Office 组合(宿主代码挂载,profile 层停不掉它的行)里那个载荷查询。
|
|
86
|
-
denyTools: ["load_workspace_dependencies"],
|
|
87
|
+
denyTools: ["load_workspace_dependencies", "plugin_manager"],
|
|
87
88
|
// 官方 Office 技能的三个名字同样按会话排除:技能目录里不列它们,`skill` 工具加载它们也被拒。
|
|
88
89
|
// 名单是技能名这一层的模型可见契约(官方 `skill-office` 的 `SKILL_NAMES`),跟着上游改名走。
|
|
89
90
|
denySkills: ["office-docx", "office-pptx", "office-xlsx"],
|
package/src/scope.ts
CHANGED
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
//
|
|
4
4
|
// `apply` 落到这个 agent 身上四件事:
|
|
5
5
|
//
|
|
6
|
-
// -
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// - **工具面**(`agent.ctx.tools.restrict()`):目录与执行走同一个视图——收窄掉的工具既不在模型目录里,
|
|
7
|
+
// 调用也直接是 `UNKNOWN_TOOL`(上游 0.2.1-rc.2 起工具声明不再经 `system-prompt/assemble`,那里只剩
|
|
8
|
+
// sections / contexts / variables,所以只能落在注册表视图上;取舍见 `../.agents/adrs/20261026-工具面收窄改用tools.restrict.md`);
|
|
9
|
+
// - **`tool:<工具名>` section**:装配瀑布里按同一份名单过滤(工具自己的说明与目录同源);
|
|
10
|
+
// - **执行层 guard**:只留给技能面——`skill` 工具本身照旧可用,被收窄的是它这次要加载的那一件技能;
|
|
9
11
|
// - **三个抑制面**(`agent/pre-step`):官方 `agent-instructions`、`skill-catalog` 与 `time-context` 的注入按开关
|
|
10
12
|
// 丢掉——官方那几行在装配投影之外自己往瀑布里 append,只能在瀑布上丢(不动注册表、也不动它们的行);
|
|
11
13
|
// - **另两个开关**(注入面那个 `skills` 在上面那条里):`runtimeContext: false` 抑制动态快照
|
|
12
14
|
// (`systemPrompt.suppressRuntimeContext`)与时钟那条注入(`agent/pre-step` 上的 `time-context`),
|
|
13
15
|
// `instructions: false` 关掉通道自己的降级注入(`ctx.contextAssembler.setInstructions`)。
|
|
14
16
|
//
|
|
15
|
-
// 同一个 agent 再 `apply` 就是换一份(旧 disposer
|
|
16
|
-
// 也不建 preset 子树。
|
|
17
|
+
// 同一个 agent 再 `apply` 就是换一份(旧 disposer 全部收回);不建 preset 子树。
|
|
17
18
|
|
|
18
19
|
import { type Context } from "@deepseek-ai/cordis";
|
|
19
20
|
import type { Agent, PreStepDecision } from "@deepseek-ai/dsh-agent";
|
|
@@ -45,21 +46,10 @@ export interface SessionScopeDefinition {
|
|
|
45
46
|
readonly runtimeContext: boolean;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
// 一条工具被拒的两类原因:白名单外 /
|
|
49
|
+
// 一条工具被拒的两类原因:白名单外 / 黑名单内。技能面文案用(工具面被收窄后不走这里,调用直接是
|
|
50
|
+
// `UNKNOWN_TOOL`——见头注释)。
|
|
49
51
|
type Denial = "not-allowlisted" | "denylisted";
|
|
50
52
|
|
|
51
|
-
const OUT_OF_SCOPE = (toolName: string, modeName: string): string =>
|
|
52
|
-
`${toolName} 不在「${modeName}」的工具白名单里,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
53
|
-
|
|
54
|
-
const DENYLISTED = (toolName: string, modeName: string): string =>
|
|
55
|
-
`${toolName} 被「${modeName}」的工具黑名单排除,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
56
|
-
|
|
57
|
-
function refusalOf(toolName: string, modeName: string, denial: Denial): string {
|
|
58
|
-
return denial === "denylisted"
|
|
59
|
-
? DENYLISTED(toolName, modeName)
|
|
60
|
-
: OUT_OF_SCOPE(toolName, modeName);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
53
|
// 技能被拒的两类原因:同一套白名单 / 黑名单语义,文案换到技能面(拒绝加载某一件技能,而不是某个工具)。
|
|
64
54
|
const SKILL_OUT_OF_SCOPE = (skillName: string, modeName: string): string =>
|
|
65
55
|
`${skillName} 不在「${modeName}」的技能白名单里,加载它不会有结果;按当前模式可用的技能完成任务,或让用户切到别的模式。`;
|
|
@@ -194,7 +184,6 @@ export class SessionScope {
|
|
|
194
184
|
? denialOf(gate, section.name.slice(TOOL_SECTION_PREFIX.length)) === undefined
|
|
195
185
|
: true,
|
|
196
186
|
),
|
|
197
|
-
tools: result.tools.filter((tool) => denialOf(gate, tool.name) === undefined),
|
|
198
187
|
};
|
|
199
188
|
});
|
|
200
189
|
}
|
|
@@ -234,24 +223,52 @@ export class SessionScope {
|
|
|
234
223
|
// service 在 `agent.ctx` 上是 shadow:抑制落在该 agent 的 scope 层,只覆盖这个会话。
|
|
235
224
|
stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
|
|
236
225
|
}
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
226
|
+
// 工具面必须落在 `agent.ctx` 上才是这个会话的作用域。`tools` 服务通常已激活(agent 建好就有),
|
|
227
|
+
// 那时**同步**注册:装配瀑布读的目录必须立刻是收窄后的那一份;没激活才退回 `inject`(它的回调晚一步,
|
|
228
|
+
// 由下一次装配兜住)。注册返回的 disposer 自己收——`restrict` / `guard` 的 effect 归 tools 服务的 ctx,
|
|
229
|
+
// 不随本 effect 收回。
|
|
230
|
+
const install = (scope: Context): (() => void)[] => {
|
|
231
|
+
const disposers: (() => void)[] = [];
|
|
232
|
+
if (gate !== undefined) {
|
|
233
|
+
// 工具面:收窄**注册表视图**(目录与执行同一份)。名单里没注册的名字先滤掉——`restrict` 对未知的
|
|
234
|
+
// 全局工具名直接抛,而"行清单里缺工具的 preset"是合法部署形态;白名单过滤后可能为空数组,那正是
|
|
235
|
+
// "一件都收不到"的语义(与省略 `allow` 的"全部放行"区分开)。
|
|
236
|
+
const known = new Set(scope.tools.definitions(agent).map((tool) => tool.name));
|
|
237
|
+
disposers.push(
|
|
238
|
+
scope.tools.restrict({
|
|
239
|
+
...(gate.allow === undefined
|
|
240
|
+
? {}
|
|
241
|
+
: { allow: [...gate.allow].filter((name) => known.has(name)) }),
|
|
242
|
+
...(gate.deny.size === 0
|
|
243
|
+
? {}
|
|
244
|
+
: { deny: [...gate.deny].filter((name) => known.has(name)) }),
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (skillGate === undefined) return disposers;
|
|
249
|
+
disposers.push(
|
|
250
|
+
scope.tools.guard((exec) => {
|
|
251
|
+
// 技能面:`skill` 工具本身照旧可用,被收窄的是它这次要加载的那一件技能。
|
|
252
|
+
if (exec.name !== SKILL_TOOL_NAME) return undefined;
|
|
253
|
+
const skillName = skillNameOf(exec.arguments);
|
|
254
|
+
if (skillName === undefined) return undefined;
|
|
255
|
+
const skillDenial = denialOf(skillGate, skillName);
|
|
256
|
+
return skillDenial === undefined
|
|
257
|
+
? undefined
|
|
258
|
+
: skillRefusalOf(skillName, definition.name, skillDenial);
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
return disposers;
|
|
262
|
+
};
|
|
263
|
+
if (agent.ctx.get("tools") === undefined) {
|
|
264
|
+
const fiber = agent.ctx.inject(["tools"], (scope) => {
|
|
265
|
+
stoppers.push(...install(scope));
|
|
252
266
|
});
|
|
253
|
-
|
|
254
|
-
|
|
267
|
+
stoppers.push(() => fiber.dispose());
|
|
268
|
+
} else {
|
|
269
|
+
stoppers.push(...install(agent.ctx));
|
|
270
|
+
}
|
|
271
|
+
return stoppers;
|
|
255
272
|
}, "session-mode: per-session gate");
|
|
256
273
|
|
|
257
274
|
// 通道那一侧的降级注入按 agent 覆盖,不需要收回。
|