@morlay/dsh-session-mode 0.0.15 → 0.0.16
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 +20 -7
- package/dist/client.cjs +1114 -1406
- package/dist/client.d.cts +41 -43
- package/dist/client.d.mts +41 -43
- package/dist/client.mjs +952 -1233
- package/dist/index.cjs +748 -70
- package/dist/index.d.mts +26 -55
- package/dist/index.mjs +742 -65
- package/dist/mode-config-CiQfAy4G.mjs +99 -0
- package/dist/mode-config-DOQ1VjcM.cjs +225 -0
- package/dist/rows-BT6Fpq-E.cjs +99 -0
- package/dist/rows-DnHpp-Ak.mjs +82 -0
- package/dist/rows.cjs +3 -86
- package/dist/rows.d.mts +31 -1
- package/dist/rows.mjs +1 -86
- package/dist/shared-R2j0SuWy.d.mts +118 -0
- package/package.json +17 -3
- package/src/client/SessionModePanel.tsx +803 -0
- package/src/client/api.ts +125 -1
- package/src/client/hints.ts +41 -0
- package/src/client/index.ts +71 -55
- package/src/client/locales.ts +80 -0
- package/src/index.ts +581 -20
- package/src/mode-config.ts +104 -0
- package/src/mode-sources.ts +4 -4
- package/src/mode-store.ts +53 -0
- package/src/modes.ts +23 -28
- package/src/per-agent.ts +53 -0
- package/src/ptc-describe.ts +97 -0
- package/src/ptc-sdk.ts +90 -0
- package/src/rows.ts +4 -8
- package/src/scope.ts +369 -71
- package/src/session-mode-store.ts +20 -0
- package/src/shared.ts +128 -3
- package/src/system-remainder.ts +39 -0
- package/src/tool-groups.ts +19 -0
- package/dist/shared-DO1nZbJ0.cjs +0 -64
- package/dist/shared-vjlwxiMt.mjs +0 -4
- package/src/client/BundleConfigPage.tsx +0 -631
- package/src/client/bundle-config.ts +0 -701
package/src/scope.ts
CHANGED
|
@@ -19,6 +19,12 @@
|
|
|
19
19
|
import { type Context } from "@deepseek-ai/cordis";
|
|
20
20
|
import type { Agent, PreStepDecision } from "@deepseek-ai/dsh-agent";
|
|
21
21
|
import type { UserMessage } from "@deepseek-ai/dsh-llm";
|
|
22
|
+
import { defineTool, projectToolSchema, type ToolDefinition } from "@deepseek-ai/dsh-tools";
|
|
23
|
+
import { renderSkeleton, replaceToolsSdk } from "./ptc-sdk.ts";
|
|
24
|
+
import { describeTools } from "./ptc-describe.ts";
|
|
25
|
+
import { perAgentAccount, type PerAgentAccount } from "./per-agent.ts";
|
|
26
|
+
import { groupTools } from "./tool-groups.ts";
|
|
27
|
+
import { isProtectedTool, refGroup, toolGroupOf } from "./shared.ts";
|
|
22
28
|
// 技能目录那条消息的 `source` 形状由官方 `tool-skill` 行声明(`MessageSourceMap` 的合并扩展):这里只借它的类型,
|
|
23
29
|
// 认它仍是按 `kind` 字符串读——运行期不依赖那一行在场。
|
|
24
30
|
import type {} from "@deepseek-ai/dsh-tool-skill";
|
|
@@ -34,6 +40,10 @@ export interface SessionScopeDefinition {
|
|
|
34
40
|
readonly allowTools: readonly string[];
|
|
35
41
|
// 这个会话明确排除的工具。与 `allowTools` 同时命中一个名字时**黑名单优先**(拒,且文案说黑名单)。
|
|
36
42
|
readonly denyTools: readonly string[];
|
|
43
|
+
// 会话级逐条目三态里"允许"的那些(组引用展开之后单独放回来)。
|
|
44
|
+
readonly allowToolsOverride?: readonly string[];
|
|
45
|
+
// 会话级逐条目三态里"禁止"的那些。
|
|
46
|
+
readonly denyToolsOverride?: readonly string[];
|
|
37
47
|
// 这个会话能用的技能;其余既不进技能目录,`skill` 工具加载它也被拒。**留空表示不设白名单**——起点是全部技能。
|
|
38
48
|
readonly allowSkills: readonly string[];
|
|
39
49
|
// 这个会话明确排除的技能。与 `allowSkills` 同时命中一个名字时**黑名单优先**(拒,且文案说黑名单)。
|
|
@@ -43,6 +53,7 @@ export interface SessionScopeDefinition {
|
|
|
43
53
|
// 这个会话要不要技能目录(`false` → 丢掉官方 `skill-catalog` 的注入)。
|
|
44
54
|
readonly skills: boolean;
|
|
45
55
|
// 这个会话要不要动态快照(`false` → 按 scope 抑制,并在瀑布上丢掉时钟那条注入)。
|
|
56
|
+
readonly dropSections: readonly string[];
|
|
46
57
|
readonly runtimeContext: boolean;
|
|
47
58
|
}
|
|
48
59
|
|
|
@@ -66,6 +77,10 @@ function skillRefusalOf(skillName: string, modeName: string, denial: Denial): st
|
|
|
66
77
|
// 单个工具的说明 section 名前缀:`tool:<工具名>`(`tools:` 那类是聚合块,不归收口管)。
|
|
67
78
|
const TOOL_SECTION_PREFIX = "tool:";
|
|
68
79
|
|
|
80
|
+
// 归"动态快照"的两条 harness 平台说明 section(实现检出路径、Web GUI 朝向)。它们由上游
|
|
81
|
+
// `harness-dev-context` 注册,`suppressRuntimeContext()` 管不到 section,只能在这里丢。
|
|
82
|
+
const RUNTIME_SECTIONS: ReadonlySet<string> = new Set(["harness:source", "app:web-surface"]);
|
|
83
|
+
|
|
69
84
|
// 加载技能的那个官方工具名:官方 `tool-skill` 行的注册名,本包不 import 那一行(依赖面只到服务),所以按字符串读
|
|
70
85
|
// ——与 `kindOf` 读官方注入面同一做法。
|
|
71
86
|
const SKILL_TOOL_NAME = "skill";
|
|
@@ -156,15 +171,46 @@ function skillNameOf(arguments_: unknown): string | undefined {
|
|
|
156
171
|
|
|
157
172
|
// 一个 agent 当前装着的那一份:装配期过滤读它(`apply` 时解析好的名单)。
|
|
158
173
|
interface Applied {
|
|
159
|
-
//
|
|
160
|
-
|
|
174
|
+
// 两条名单都空且没有待放行的按需组 = 不过滤(装配期与执行层都放行)。组引用在拿到工具名全集后展开,
|
|
175
|
+
// 所以它是可替换的:按需面重算后按新的名单重建。
|
|
176
|
+
gate: NameGate | undefined;
|
|
177
|
+
// 展开后的两条名单:重算 `gate` 的底稿(按需面变了,显式名单不变)。
|
|
178
|
+
readonly allowExpanded: readonly string[];
|
|
179
|
+
readonly denyExpanded: readonly string[];
|
|
180
|
+
// 工具名全集:判断某个 MCP server 的指令还要不要留在提示词里。
|
|
181
|
+
readonly known: readonly string[];
|
|
182
|
+
// 这个会话要不要动态快照(`false` 时把 harness 的平台说明 section 一并去掉)。
|
|
183
|
+
readonly dropSections: readonly string[];
|
|
184
|
+
readonly runtimeContext: boolean;
|
|
161
185
|
// 收回这一份在 `agent.ctx` 上的注册(抑制器与执行层 guard)。
|
|
162
186
|
readonly dispose: () => void;
|
|
163
187
|
}
|
|
164
188
|
|
|
189
|
+
// 一个会话当前的工具面:名字全集、模式自己的黑名单、按需分组,以及那次 `restrict` 的收回句柄。
|
|
190
|
+
// `restrict` 会随"放行一组"重建,所以它单独存,不跟控制工具一起收。
|
|
191
|
+
interface ToolFace {
|
|
192
|
+
// 放行一组时要回写这个 agent 的装配期名单(说明 section 与目录同源)。
|
|
193
|
+
readonly agent: Agent;
|
|
194
|
+
readonly known: readonly string[];
|
|
195
|
+
readonly baseDeny: ReadonlySet<string>;
|
|
196
|
+
readonly groups: ReadonlyMap<string, readonly string[]>;
|
|
197
|
+
readonly allowOverride: ReadonlySet<string>;
|
|
198
|
+
readonly denyOverride: ReadonlySet<string>;
|
|
199
|
+
disposeRestriction?: (() => void) | undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
165
202
|
// 按会话收口工具面与注入面的状态机:一份 per-agent 的装配期名单 + 挂在各 agent 上的注册。
|
|
166
203
|
export class SessionScope {
|
|
167
204
|
private readonly applied = new WeakMap<Agent, Applied>();
|
|
205
|
+
// 会话内已放行的 MCP 组(换模式重建工具面时保留)。
|
|
206
|
+
private readonly activatedGroups = new WeakMap<Agent, Set<string>>();
|
|
207
|
+
// 会话当前的工具面(名字全集、基础黑名单、分组与 restrict 句柄)。
|
|
208
|
+
private readonly faces = new WeakMap<Agent, ToolFace>();
|
|
209
|
+
// 全量工具目录(第一次收口前抓,之后复用):面板要的 id + 描述。
|
|
210
|
+
private readonly catalogs = new WeakMap<
|
|
211
|
+
Agent,
|
|
212
|
+
readonly { name: string; description: string }[]
|
|
213
|
+
>();
|
|
168
214
|
private readonly ctx: Context;
|
|
169
215
|
|
|
170
216
|
constructor(ctx: Context) {
|
|
@@ -173,21 +219,56 @@ export class SessionScope {
|
|
|
173
219
|
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
174
220
|
const agent = context.agent;
|
|
175
221
|
const applied = agent === undefined ? undefined : this.applied.get(agent);
|
|
176
|
-
if (applied === undefined
|
|
222
|
+
if (applied === undefined) return next();
|
|
223
|
+
|
|
177
224
|
const gate = applied.gate;
|
|
178
225
|
const result = await next();
|
|
226
|
+
const filtered = result.sections.filter((section) => {
|
|
227
|
+
// 工具自己的说明 section 与工具目录同源:目录里没有的工具,它的说明也不该留在提示词里。
|
|
228
|
+
// 动态快照关掉时,harness 的平台说明(实现检出路径与 Web GUI 朝向)也不该留在提示词里。
|
|
229
|
+
if (!applied.runtimeContext && RUNTIME_SECTIONS.has(section.name)) return false;
|
|
230
|
+
// 模式配置的 `dropSections`:精确名,或以 `*` 结尾的前缀(例如 `tool:*`)。
|
|
231
|
+
if (
|
|
232
|
+
applied.dropSections.some((pattern) =>
|
|
233
|
+
pattern.endsWith("*")
|
|
234
|
+
? section.name.startsWith(pattern.slice(0, -1))
|
|
235
|
+
: section.name === pattern,
|
|
236
|
+
)
|
|
237
|
+
) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
if (gate === undefined) return true;
|
|
241
|
+
if (gate !== undefined && section.name.startsWith(TOOL_SECTION_PREFIX)) {
|
|
242
|
+
return denialOf(gate, section.name.slice(TOOL_SECTION_PREFIX.length)) === undefined;
|
|
243
|
+
}
|
|
244
|
+
// MCP server 的指令是一条 `mcp:<server>` section:整组工具都不可见时,它也不该留在提示词里。
|
|
245
|
+
if (gate !== undefined && section.name.startsWith("mcp:")) {
|
|
246
|
+
const server = section.name.slice("mcp:".length);
|
|
247
|
+
return applied.known.some(
|
|
248
|
+
(name) => name.startsWith(`mcp__${server}__`) && denialOf(gate, name) === undefined,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return true;
|
|
252
|
+
});
|
|
179
253
|
return {
|
|
180
254
|
...result,
|
|
181
|
-
//
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
),
|
|
255
|
+
// PTC 装配:上游 `tools:sdk` 会把全部工具铺成一张大表(工具一变、system 前缀就变)。
|
|
256
|
+
// 这里换成稳定骨架,具体分组由 `describe_tool` 现取。
|
|
257
|
+
sections: replaceToolsSdk(
|
|
258
|
+
[...filtered],
|
|
259
|
+
renderSkeleton([...new Set(applied.known.map((name) => toolGroupOf(name)))].sort()),
|
|
260
|
+
) as typeof result.sections,
|
|
187
261
|
};
|
|
188
262
|
});
|
|
189
263
|
}
|
|
190
264
|
|
|
265
|
+
// 一份新的 per-agent 收回账(见 `./per-agent.ts`)。收口之外的 per-agent 注册(persona、默认模型兜底,
|
|
266
|
+
// 由 `SessionModes.installFor` 装)也走它——它们同样落在 `agent.ctx` 上,必须在本行重挂之前一起收干净。
|
|
267
|
+
// 这里的 `ctx` 是**本行**的:构造时记下的那份,不受服务追踪代理影响。
|
|
268
|
+
account(agent: Agent): PerAgentAccount {
|
|
269
|
+
return perAgentAccount(this.ctx, agent);
|
|
270
|
+
}
|
|
271
|
+
|
|
191
272
|
// 把某个会话收口到这份定义上(同一个 agent 再调就是换一份)。
|
|
192
273
|
apply(agent: Agent, definition: SessionScopeDefinition): void {
|
|
193
274
|
const previous = this.applied.get(agent);
|
|
@@ -196,56 +277,72 @@ export class SessionScope {
|
|
|
196
277
|
const gate = gateOfNames(definition.allowTools, definition.denyTools);
|
|
197
278
|
const skillGate = gateOfNames(definition.allowSkills, definition.denySkills);
|
|
198
279
|
|
|
199
|
-
// 落在 `agent.ctx`
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
{
|
|
220
|
-
|
|
280
|
+
// 落在 `agent.ctx` 上的那几件记在一条收回账上(`./per-agent.ts`):这些注册的 effect 归 **agent 的 scope
|
|
281
|
+
// fiber**,不随本行卸载,所以账也挂在本行上——行重挂/卸载时先把它们收干净,否则新实例再装一次就撞名。
|
|
282
|
+
// 收回是**同步**的:`apply` 重入时「旧的收完再装新的」靠的就是这一点(`inject` fiber 的 `dispose()` 是
|
|
283
|
+
// 异步的,只能算链上的一笔)。
|
|
284
|
+
const account = this.account(agent);
|
|
285
|
+
|
|
286
|
+
// 三个抑制面:官方 `agent-instructions`(工作区指令)、官方 `skill-catalog`(技能目录)与官方 `time-context`
|
|
287
|
+
// (时钟)都在装配投影之外自己往这条瀑布里 append,所以只能在这里丢它们的条目(技能目录那条还要按名收窄)。
|
|
288
|
+
//
|
|
289
|
+
// 两件事让这条注册必须落在**会话**上而不是行上:
|
|
290
|
+
// - `prepend` 只是把自己排到表头,不保证一直在那儿:表头归**最后**注册的那个 prepend,而时钟那条也 prepend;
|
|
291
|
+
// - 官方那几行随 preset 挂载(早于 `agent/created`,本注册就在那儿),所以按会话注册天然排在它们之后。
|
|
292
|
+
// 换模式 / 换 preset 时 `apply` 会重来一遍(旧的收回、新的 prepend),最外层由此回到我们手上。
|
|
293
|
+
account.add(
|
|
294
|
+
agent.ctx.on(
|
|
295
|
+
"agent/pre-step",
|
|
296
|
+
async (_payload, next): Promise<PreStepDecision> => {
|
|
297
|
+
const decision = await next();
|
|
298
|
+
if (decision.kind !== "enter") return decision;
|
|
299
|
+
const kept = this.suppress(decision.messages, definition, skillGate);
|
|
300
|
+
return kept === undefined ? decision : { ...decision, messages: kept };
|
|
301
|
+
},
|
|
302
|
+
{ prepend: true },
|
|
303
|
+
),
|
|
304
|
+
);
|
|
305
|
+
if (definition.runtimeContext === false) {
|
|
306
|
+
// service 在 `agent.ctx` 上是 shadow:抑制落在该 agent 的 scope 层,只覆盖这个会话。
|
|
307
|
+
account.add(agent.ctx.systemPrompt.suppressRuntimeContext());
|
|
308
|
+
}
|
|
309
|
+
// 工具面必须落在 `agent.ctx` 上才是这个会话的作用域。`tools` 服务通常已激活(agent 建好就有),
|
|
310
|
+
// 那时**同步**注册:装配瀑布读的目录必须立刻是收窄后的那一份;没激活才退回 `inject`(它的回调晚一步,
|
|
311
|
+
// 由下一次装配兜住)。注册返回的 disposer 一起记在这条账上。
|
|
312
|
+
const install = (scope: Context): (() => void)[] => {
|
|
313
|
+
const disposers: (() => void)[] = [];
|
|
314
|
+
// 工具面:收窄**注册表视图**(目录与执行同一份)。名单里没注册的名字先滤掉——`restrict` 对未知的
|
|
315
|
+
// 全局工具名直接抛,而"行清单里缺工具的 preset"是合法部署形态;白名单过滤后可能为空数组,那正是
|
|
316
|
+
// "一件都收不到"的语义(与省略 `allow` 的"全部放行"区分开)。
|
|
317
|
+
const definitions = scope.tools.definitions(agent);
|
|
318
|
+
const known = definitions.map((tool) => tool.name);
|
|
319
|
+
this.catalogs.set(
|
|
320
|
+
agent,
|
|
321
|
+
definitions.map((tool) => ({ name: tool.name, description: tool.description })),
|
|
221
322
|
);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}),
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
if (skillGate === undefined) return disposers;
|
|
323
|
+
// 按需组默认不可见:模型要用它们时走 `describe_tool({ group })` 取说明与签名,再在 `run_code`
|
|
324
|
+
// 里 `await tools.name(...)` 调(PTC 下没有"直接调用"的入口)。常驻组(`isResidentToolGroup`)
|
|
325
|
+
// 不在 `groups` 里,它们一直留在目录。
|
|
326
|
+
// 设了白名单的模式不做按需——那些模式的意图本来就是"只留这几件"。
|
|
327
|
+
const groups =
|
|
328
|
+
gate === undefined || gate.allow === undefined
|
|
329
|
+
? groupTools(known)
|
|
330
|
+
: new Map<string, string[]>();
|
|
331
|
+
const activated = this.activatedGroups.get(agent) ?? new Set<string>();
|
|
332
|
+
this.activatedGroups.set(agent, activated);
|
|
333
|
+
const face: ToolFace = {
|
|
334
|
+
agent,
|
|
335
|
+
known,
|
|
336
|
+
baseDeny: new Set(definition.denyTools),
|
|
337
|
+
allowOverride: new Set(definition.allowToolsOverride ?? []),
|
|
338
|
+
denyOverride: new Set(definition.denyToolsOverride ?? []),
|
|
339
|
+
groups,
|
|
340
|
+
};
|
|
341
|
+
this.faces.set(agent, face);
|
|
342
|
+
// `describe_tool` 常驻:PTC 程序里随时可以取某一组的 d.ts。
|
|
343
|
+
disposers.push(scope.tools.register(this.toolsDescribeTool(scope)));
|
|
344
|
+
this.restrict(scope, gate, face, activated);
|
|
345
|
+
if (skillGate !== undefined) {
|
|
249
346
|
disposers.push(
|
|
250
347
|
scope.tools.guard((exec) => {
|
|
251
348
|
// 技能面:`skill` 工具本身照旧可用,被收窄的是它这次要加载的那一件技能。
|
|
@@ -258,25 +355,226 @@ export class SessionScope {
|
|
|
258
355
|
: skillRefusalOf(skillName, definition.name, skillDenial);
|
|
259
356
|
}),
|
|
260
357
|
);
|
|
261
|
-
return disposers;
|
|
262
|
-
};
|
|
263
|
-
if (agent.ctx.get("tools") === undefined) {
|
|
264
|
-
const fiber = agent.ctx.inject(["tools"], (scope) => {
|
|
265
|
-
stoppers.push(...install(scope));
|
|
266
|
-
});
|
|
267
|
-
stoppers.push(() => fiber.dispose());
|
|
268
|
-
} else {
|
|
269
|
-
stoppers.push(...install(agent.ctx));
|
|
270
358
|
}
|
|
271
|
-
return
|
|
272
|
-
|
|
359
|
+
return [
|
|
360
|
+
...disposers,
|
|
361
|
+
() => {
|
|
362
|
+
face.disposeRestriction?.();
|
|
363
|
+
face.disposeRestriction = undefined;
|
|
364
|
+
},
|
|
365
|
+
];
|
|
366
|
+
};
|
|
367
|
+
if (agent.ctx.get("tools") === undefined) {
|
|
368
|
+
// 回调晚一步:装出来的那一份照旧记在这条账上。账已经收过时 `add` 会就地收掉它——行重挂/换模式的竞态
|
|
369
|
+
// 因此不会留下孤儿注册。
|
|
370
|
+
const fiber = agent.ctx.inject(["tools"], (scope) => {
|
|
371
|
+
for (const stop of install(scope)) account.add(stop);
|
|
372
|
+
});
|
|
373
|
+
account.add(() => {
|
|
374
|
+
// fiber 的卸载是异步的(它要等子 fiber 卸载),这里只负责让它别再跑;它名下那些注册已经由上面收完了。
|
|
375
|
+
void fiber.dispose().catch((error: unknown) => this.ctx.logger.error(error));
|
|
376
|
+
});
|
|
377
|
+
} else {
|
|
378
|
+
for (const stop of install(agent.ctx)) account.add(stop);
|
|
379
|
+
}
|
|
273
380
|
|
|
274
381
|
// 通道那一侧的降级注入按 agent 覆盖,不需要收回。
|
|
275
382
|
this.channel()?.setInstructions(agent, definition.instructions);
|
|
276
383
|
|
|
384
|
+
// 组引用要用工具名全集展开后才是装配期过滤能认的名字;known 由 install 抓到(`faces`)。
|
|
385
|
+
const known = this.faces.get(agent)?.known ?? [];
|
|
386
|
+
const expand = (names: readonly string[]): string[] => {
|
|
387
|
+
const out = new Set<string>();
|
|
388
|
+
for (const ref of names) {
|
|
389
|
+
if (ref.startsWith("group:") || ref.startsWith("mcp:")) {
|
|
390
|
+
const group = refGroup(ref);
|
|
391
|
+
for (const tool of known) if (toolGroupOf(tool) === group) out.add(tool);
|
|
392
|
+
} else {
|
|
393
|
+
out.add(ref);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return [...out];
|
|
397
|
+
};
|
|
398
|
+
const expandedDeny = expand(definition.denyTools);
|
|
399
|
+
for (const id of definition.allowToolsOverride ?? []) {
|
|
400
|
+
const at = expandedDeny.indexOf(id);
|
|
401
|
+
if (at >= 0) expandedDeny.splice(at, 1);
|
|
402
|
+
}
|
|
403
|
+
for (const id of definition.denyToolsOverride ?? []) {
|
|
404
|
+
if (!expandedDeny.includes(id)) expandedDeny.push(id);
|
|
405
|
+
}
|
|
406
|
+
const allowExpanded = expand(definition.allowTools);
|
|
277
407
|
this.applied.set(agent, {
|
|
278
|
-
|
|
279
|
-
|
|
408
|
+
// 按需面里还没放行的工具也要进 deny:否则它们的 `tool:` 说明会留在提示词里,而目录里没有它们。
|
|
409
|
+
gate: gateOfNames(allowExpanded, [...expandedDeny, ...this.onDemandDeny(agent)]),
|
|
410
|
+
allowExpanded,
|
|
411
|
+
denyExpanded: expandedDeny,
|
|
412
|
+
known,
|
|
413
|
+
dropSections: definition.dropSections ?? [],
|
|
414
|
+
runtimeContext: definition.runtimeContext,
|
|
415
|
+
dispose: () => account.release(),
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 按需面里还没放行的工具名:它们既不进目录,说明 section 也不该留。
|
|
420
|
+
private onDemandDeny(agent: Agent): string[] {
|
|
421
|
+
const face = this.faces.get(agent);
|
|
422
|
+
if (face === undefined) return [];
|
|
423
|
+
const activated = this.activatedGroups.get(agent);
|
|
424
|
+
const denied: string[] = [];
|
|
425
|
+
for (const [group, tools] of face.groups) {
|
|
426
|
+
if (activated?.has(group) === true) continue;
|
|
427
|
+
denied.push(...tools);
|
|
428
|
+
}
|
|
429
|
+
return denied;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// 这个会话在模式名单收窄之后"程序内可用"的工具名(注册表 ∩ 模式名单)。
|
|
433
|
+
knownTools(agent: Agent): readonly string[] {
|
|
434
|
+
const applied = this.applied.get(agent);
|
|
435
|
+
if (applied === undefined) return [];
|
|
436
|
+
// 注册表 ∩ 模式收窄:这才是"程序内可用"的那一层(模型面再被 PTC 投影成 `run_code`)。
|
|
437
|
+
// 实时读注册表(`applied.known` 是 apply 那一刻的快照,之后新注册的工具也要算)。
|
|
438
|
+
const tools = this.ctx.get("tools") as unknown as
|
|
439
|
+
| { definitions(scope: Agent): readonly { name: string }[] }
|
|
440
|
+
| undefined;
|
|
441
|
+
const names = (tools?.definitions(agent) ?? applied.known.map((name) => ({ name }))).map(
|
|
442
|
+
(definition) => definition.name,
|
|
443
|
+
);
|
|
444
|
+
const gate = applied.gate;
|
|
445
|
+
const kept =
|
|
446
|
+
gate === undefined ? names : names.filter((tool) => denialOf(gate, tool) === undefined);
|
|
447
|
+
return kept;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// 全量工具目录(id + 描述):面板的"工具"折叠组读它。
|
|
451
|
+
catalogOf(agent: Agent): readonly { name: string; description: string }[] {
|
|
452
|
+
return this.catalogs.get(agent) ?? [];
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 按当前已放行的组重算工具面:模式白名单与基础黑名单原样,未放行那一组的工具进 deny。
|
|
456
|
+
private restrict(
|
|
457
|
+
scope: Context,
|
|
458
|
+
gate: NameGate | undefined,
|
|
459
|
+
face: ToolFace,
|
|
460
|
+
activated: ReadonlySet<string>,
|
|
461
|
+
): void {
|
|
462
|
+
face.disposeRestriction?.();
|
|
463
|
+
face.disposeRestriction = undefined;
|
|
464
|
+
// 名单里可以引用整组(`mcp:<server>` / `group:<id>`):先展开成组内工具,再按组放行。
|
|
465
|
+
const denied = new Set<string>();
|
|
466
|
+
for (const ref of face.baseDeny) {
|
|
467
|
+
if (ref.startsWith("group:") || ref.startsWith("mcp:")) {
|
|
468
|
+
const group = refGroup(ref);
|
|
469
|
+
for (const tool of face.known) if (toolGroupOf(tool) === group) denied.add(tool);
|
|
470
|
+
} else {
|
|
471
|
+
denied.add(ref);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// 逐条目三态在组引用展开之后应用:显式允许把名字从 deny 里拿掉,显式禁止加进去。
|
|
475
|
+
for (const [group, tools] of face.groups) {
|
|
476
|
+
if (activated.has(group)) continue;
|
|
477
|
+
for (const tool of tools) denied.add(tool);
|
|
478
|
+
}
|
|
479
|
+
for (const id of face.allowOverride) denied.delete(id);
|
|
480
|
+
for (const id of face.denyOverride) denied.add(id);
|
|
481
|
+
// 受保护的工具永远不被 deny:`run_code` 是 PTC 入口,`describe_tool` 是取签名的手段。
|
|
482
|
+
for (const id of denied) {
|
|
483
|
+
if (isProtectedTool(id)) denied.delete(id);
|
|
484
|
+
}
|
|
485
|
+
if (gate?.allow === undefined && denied.size === 0) return;
|
|
486
|
+
const known = new Set(face.known);
|
|
487
|
+
// 白名单模式下也不收 `run_code` / `describe_tool`:它们是 PTC 的入口与取签名手段。
|
|
488
|
+
const allow =
|
|
489
|
+
gate?.allow === undefined
|
|
490
|
+
? undefined
|
|
491
|
+
: [...gate.allow, ...[...known].filter(isProtectedTool)].filter((name) => known.has(name));
|
|
492
|
+
const denyList = [...denied].filter((name) => known.has(name));
|
|
493
|
+
face.disposeRestriction = this.installRestriction(scope, allow, denyList);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 收窄注册表视图。`tools.restrict` 只认**全局**工具名(上游 `restrictableNames` 是全局与祖先层,
|
|
497
|
+
// **不含本 agent 自己**的注册);行清单里的工具由 preset 注册在这个 agent 的 scope 上,
|
|
498
|
+
// `describe_tool` 也是我们在这里注册的——名单里出现那种名字时上游会抛 `names unknown global tool`。
|
|
499
|
+
// 所以:先按原名单收窄,抛了就退回"只按全局视图里有的名字"再试一次,仍不行就跳过(收窄是增强,
|
|
500
|
+
// 不该让整行装配失败)。
|
|
501
|
+
private installRestriction(
|
|
502
|
+
scope: Context,
|
|
503
|
+
allow: readonly string[] | undefined,
|
|
504
|
+
deny: readonly string[],
|
|
505
|
+
): (() => void) | undefined {
|
|
506
|
+
const attempt = (
|
|
507
|
+
nextAllow: readonly string[] | undefined,
|
|
508
|
+
nextDeny: readonly string[],
|
|
509
|
+
): (() => void) | undefined => {
|
|
510
|
+
if (nextAllow === undefined && nextDeny.length === 0) return undefined;
|
|
511
|
+
try {
|
|
512
|
+
return scope.tools.restrict({
|
|
513
|
+
...(nextAllow === undefined ? {} : { allow: [...nextAllow] }),
|
|
514
|
+
...(nextDeny.length === 0 ? {} : { deny: [...nextDeny] }),
|
|
515
|
+
});
|
|
516
|
+
} catch (error) {
|
|
517
|
+
this.ctx.logger.debug(`session-mode: 工具面收窄降级(${String(error)})`);
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const first = attempt(allow, deny);
|
|
522
|
+
if (first !== undefined || (allow === undefined && deny.length === 0)) return first;
|
|
523
|
+
// 降级:只保留全局视图里真的有的名字(那才是 `restrict` 能命中的一层)。
|
|
524
|
+
const global = this.globalToolNames();
|
|
525
|
+
return attempt(
|
|
526
|
+
allow === undefined ? undefined : allow.filter((name) => global.has(name)),
|
|
527
|
+
deny.filter((name) => global.has(name)),
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// 全局层的工具名:`tools.restrict` 能命中的那一层。
|
|
532
|
+
private globalToolNames(): ReadonlySet<string> {
|
|
533
|
+
const tools = this.ctx.get("tools") as unknown as
|
|
534
|
+
| { definitions(): readonly { name: string }[] }
|
|
535
|
+
| undefined;
|
|
536
|
+
return new Set((tools?.definitions() ?? []).map((definition) => definition.name));
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 常驻的组目录:只列组名、数量与前三个工具名。被收窄的工具定义读不到,所以这里不猜描述。
|
|
540
|
+
// PTC 程序里现取某组工具的 d.ts:`await tools.describe_tool({ group: "fs" })`。
|
|
541
|
+
// 不带 group 时返回分组清单。工具本身常驻,工具集变化只影响 describe 的结果。
|
|
542
|
+
private toolsDescribeTool(scope: Context): ToolDefinition {
|
|
543
|
+
return defineTool({
|
|
544
|
+
name: "describe_tool",
|
|
545
|
+
// PTC 下工具描述不注入(说明随 describe 结果返回),这里强制置空。
|
|
546
|
+
description: "",
|
|
547
|
+
parameters: { group: { type: "string" } },
|
|
548
|
+
output: {
|
|
549
|
+
schema: {
|
|
550
|
+
type: "object",
|
|
551
|
+
additionalProperties: false,
|
|
552
|
+
properties: { text: { type: "string", required: true } },
|
|
553
|
+
},
|
|
554
|
+
render: (_args, value) => [{ type: "text", text: value.text }],
|
|
555
|
+
},
|
|
556
|
+
async execute(args) {
|
|
557
|
+
const definitions = scope.tools.definitions();
|
|
558
|
+
// 每个工具的说明 section(`tool:<name>`)随 describe 一起返回,不占 system。
|
|
559
|
+
const prompt = scope.get("systemPrompt") as unknown as
|
|
560
|
+
| { sections?: { get(name: string): { text: unknown } | undefined } }
|
|
561
|
+
| undefined;
|
|
562
|
+
const sectionOf = (name: string): string | undefined => {
|
|
563
|
+
const section = prompt?.sections?.get(`tool:${name}`);
|
|
564
|
+
return typeof section?.text === "string" ? section.text : undefined;
|
|
565
|
+
};
|
|
566
|
+
const entries = definitions.map((definition) => ({
|
|
567
|
+
name: definition.name,
|
|
568
|
+
description: definition.description ?? "",
|
|
569
|
+
// `definition.parameters` 是 schemastery 形状;`projectToolSchema` 给出 JSON Schema,
|
|
570
|
+
// `describe_tool` 才能把它转成真实的 TS 参数类型(而不是 `JsonValue`)。
|
|
571
|
+
parametersSchema: projectToolSchema(definition, false).parameters,
|
|
572
|
+
outputSchema: definition.output?.schema,
|
|
573
|
+
section: sectionOf(definition.name),
|
|
574
|
+
}));
|
|
575
|
+
const group = typeof args.group === "string" ? args.group : undefined;
|
|
576
|
+
return { text: describeTools(entries, group) };
|
|
577
|
+
},
|
|
280
578
|
});
|
|
281
579
|
}
|
|
282
580
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// 会话级覆盖的存储:复用 harness 的 storageDomain(与 `session_projcache` 同一层),按 sessionId 存一份
|
|
2
|
+
// `ModeConfig`。不写会话日志、不自己落裸文件。
|
|
3
|
+
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
6
|
+
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
7
|
+
|
|
8
|
+
export const sessionModeRecord = z.object({
|
|
9
|
+
config: z.record(z.string(), z.unknown()),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const sessionModeDomainSpec = defineDomain({
|
|
13
|
+
name: "session_mode",
|
|
14
|
+
version: 1,
|
|
15
|
+
invalidRecords: "backup-and-skip",
|
|
16
|
+
layout: "per-record",
|
|
17
|
+
tables: {
|
|
18
|
+
sessions: domainTable<SessionId, { config: Record<string, unknown> }>(sessionModeRecord),
|
|
19
|
+
},
|
|
20
|
+
});
|