@morlay/dsh-context-assembler 0.0.1-alpha.2 → 0.0.1-alpha.4
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 +59 -32
- package/dist/{agent-instructions-BNEiZl0P.mjs → agent-instructions-CIgUVIeo.mjs} +36 -1
- package/dist/agent-instructions.mjs +1 -1
- package/dist/{assembler-_F9urbPj.mjs → assembler-CDMIBAi8.mjs} +18 -6
- package/dist/assembler.d.mts +1 -1
- package/dist/assembler.mjs +1 -1
- package/dist/{index-BGkeEaQA.d.mts → index-zhmD45A9.d.mts} +21 -4
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +3 -3
- package/dist/rows.d.mts +13 -11
- package/dist/rows.mjs +15 -10
- package/dist/scope.d.mts +33 -39
- package/dist/scope.mjs +102 -2
- package/dist/skill-catalog.d.mts +7 -0
- package/dist/skill-catalog.mjs +29 -7
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +37 -39
- package/src/agent-instructions/index.ts +6 -2
- package/src/agent-instructions/preset-owner.ts +70 -0
- package/src/assembler/channel.ts +34 -10
- package/src/assembler/reminder.ts +12 -3
- package/src/index.ts +5 -5
- package/src/rows.ts +14 -13
- package/src/scope/index.ts +131 -99
- package/src/skill-catalog/index.ts +44 -18
- package/cordis.patch.yml +0 -9
- package/dist/scope-CI-fNq5A.mjs +0 -104
- package/tool/patch.ts +0 -43
package/src/scope/index.ts
CHANGED
|
@@ -1,118 +1,150 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* 按**会话**收口:工具白名单、instruction 总开关、动态快照。
|
|
3
|
+
*
|
|
4
|
+
* 这是本包的 `scope` 出口(行 id `context-assembler-scope`)。它不再吃 config,也不认识"模式"这个概念——
|
|
5
|
+
* 收口要什么由消费方在会话建立时**推**进来:`ctx.sessionToolScope.apply(agent, mode)`(
|
|
6
|
+
* [`@morlay/dsh-session-mode`](../../../profile/dsh-session-mode/README.md) 的模式定义里正好是这几项)。
|
|
7
|
+
* 方向因此是单向的:装配层读这个服务,这个包不认识装配层。
|
|
8
|
+
*
|
|
9
|
+
* `apply` 落到该 agent 身上的三件事,各走一条与"谁注册了这个工具"无关的路:
|
|
10
|
+
*
|
|
11
|
+
* 1. **装配期投影**(每次装配按已注册的那份):模型目录与工具说明的 `tool:<工具名>` section 同源过滤;
|
|
12
|
+
* 2. **执行层 guard**:落在 `agent.ctx` 上,调用被拒时给出该模式的文案;
|
|
13
|
+
* 3. **通道开关**:`instructions` 关掉该会话的规则块(工作区指令、技能目录、用法正文)、
|
|
14
|
+
* `runtimeContext` 抑制该会话的动态快照(沙箱与审批策略那两条)。
|
|
15
|
+
*
|
|
16
|
+
* 为什么不用 `tools.restrict()`:它会改动可见工具集,从而触发 `tools/change`;上游 `tool-subagent` 正是用
|
|
17
|
+
* 该事件做 composition reconcile——两边互相触发会变成装配风暴。这里用投影过滤加执行层 guard:既不碰注册
|
|
18
|
+
* 表、也不发事件。
|
|
19
|
+
*
|
|
20
|
+
* 为什么按 `agent.ctx` 注册而不是先建一棵 preset 子树:会话的 persona / 工具收口本来就是"这个 agent 的
|
|
21
|
+
* 事",`agent.ctx` 的 scope 已经够了(上游给子 agent 装 persona 用的也是这条路);一棵子树要连带上游的
|
|
22
|
+
* `agent-presets` 注册表、每 revision 一份 Loader 与 `isolate` realm,模式本身用不上。
|
|
23
|
+
*
|
|
24
|
+
* 模式在空白窗口里可以切换,所以状态是**可替换**的:同一个 agent 再 `apply` 一次就是换一份——旧的
|
|
25
|
+
* disposer 全部收回,再按新的装一遍。
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { Service, type Context } from "@deepseek-ai/cordis";
|
|
2
29
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
3
|
-
import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
|
|
4
30
|
import z from "@deepseek-ai/schemastery";
|
|
5
31
|
import type {} from "../assembler/index.ts";
|
|
6
32
|
|
|
7
|
-
export const name = "context-scope";
|
|
33
|
+
export const name = "context-assembler-scope";
|
|
8
34
|
|
|
35
|
+
/** 收口要的服务;`sessionToolScope` 由本行自己发布,`tools` 的 guard 挂在每个 agent 的 ctx 上。 */
|
|
9
36
|
export const inject = ["tools", "systemPrompt"];
|
|
10
37
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
* 这里是"连与工具无关的 instruction 也不要"的总开关。
|
|
25
|
-
*/
|
|
26
|
-
instructions?: boolean;
|
|
27
|
-
/**
|
|
28
|
-
* 是否要 runtime context(动态快照:文件沙箱策略、审批策略)。缺省要;`false` 表示这个模式不要它们
|
|
29
|
-
* ——对话模式没有文件与 shell 工具,"能改工作区哪些文件、要不要走审批"对它全是噪音。
|
|
30
|
-
*
|
|
31
|
-
* 抑制是**按 scope** 的(上游按装配的 scope 链查抑制器),所以本行装在 preset 子树里就只作用于这个
|
|
32
|
-
* 模式的会话;它只挡注入,不改任何提供方的行为(沙箱该怎么判还怎么判)。
|
|
33
|
-
*/
|
|
34
|
-
runtimeContext?: boolean;
|
|
38
|
+
/** 本行不吃 config:要收成什么样由消费方在 `apply` 时给。 */
|
|
39
|
+
export const Config = z.object({});
|
|
40
|
+
|
|
41
|
+
/** 一次收口的全部输入:模式定义里本出口用到的那几项。 */
|
|
42
|
+
export interface SessionToolScopeMode {
|
|
43
|
+
/** 拒绝对话时用的模式名。 */
|
|
44
|
+
readonly name: string;
|
|
45
|
+
/** 这个会话能用的工具;其余既不进目录,调用也被拒。 */
|
|
46
|
+
readonly allowTools: readonly string[];
|
|
47
|
+
/** 缺省为要;`false` 表示这个会话不要任何 instruction 类注入。 */
|
|
48
|
+
readonly instructions?: boolean | undefined;
|
|
49
|
+
/** 缺省为要;`false` 表示这个会话不要动态快照。 */
|
|
50
|
+
readonly runtimeContext?: boolean | undefined;
|
|
35
51
|
}
|
|
36
52
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
53
|
+
declare module "@deepseek-ai/cordis" {
|
|
54
|
+
interface Context {
|
|
55
|
+
sessionToolScope: SessionToolScope;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
42
58
|
|
|
43
|
-
const OUT_OF_SCOPE = (toolName: string): string =>
|
|
44
|
-
`${toolName}
|
|
59
|
+
const OUT_OF_SCOPE = (toolName: string, modeName: string): string =>
|
|
60
|
+
`${toolName} 不在「${modeName}」的工具范围内,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
45
61
|
|
|
46
62
|
/** 单个工具的说明 section 名前缀:`tool:<工具名>`(`tools:` 那类是聚合块,不归白名单管)。 */
|
|
47
63
|
const TOOL_SECTION_PREFIX = "tool:";
|
|
48
64
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
*
|
|
56
|
-
* 为什么不是 `tools.restrict()`:它会改动可见工具集,从而触发 `tools/change`;上游 `tool-subagent`
|
|
57
|
-
* 正是用该事件做 composition reconcile——两边互相触发会变成装配风暴。这里用投影过滤加执行层 guard:
|
|
58
|
-
* 既不碰注册表、也不发事件。
|
|
59
|
-
*/
|
|
60
|
-
export function apply(ctx: Context, config: Config): void {
|
|
61
|
-
const allow = new Set(config.allowTools ?? []);
|
|
62
|
-
if (allow.size === 0) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
"context-scope: `allowTools` must name at least one tool; omit the row instead of mounting an empty scope",
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
const presetKey = scopeOf(ctx);
|
|
68
|
-
const scoped = new WeakSet<Agent>();
|
|
65
|
+
/** 一个 agent 当前装着的那一份。 */
|
|
66
|
+
interface Applied {
|
|
67
|
+
readonly mode: SessionToolScopeMode;
|
|
68
|
+
/** 收回这一份在 `agent.ctx` 上的注册(抑制器与执行层 guard)。 */
|
|
69
|
+
readonly dispose: () => void;
|
|
70
|
+
}
|
|
69
71
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
72
|
+
/** 通道是可选的搭档:没有它就没有 instruction 可关、也没人收窄技能目录,工具目录与执行 guard 照常生效。 */
|
|
73
|
+
function channelOf(ctx: Context):
|
|
74
|
+
| {
|
|
75
|
+
setInstructions(agent: Agent, on: boolean): void;
|
|
76
|
+
restrictTools(agent: Agent, allowed: (tool: string) => boolean): void;
|
|
77
|
+
}
|
|
78
|
+
| undefined {
|
|
79
|
+
try {
|
|
80
|
+
return ctx.get("contextAssembler");
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
73
83
|
}
|
|
84
|
+
}
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
86
|
+
/** 按会话收口工具与注入的服务。 */
|
|
87
|
+
export class SessionToolScope extends Service {
|
|
88
|
+
private readonly applied = new WeakMap<Agent, Applied>();
|
|
89
|
+
|
|
90
|
+
constructor(ctx: Context) {
|
|
91
|
+
super(ctx, "sessionToolScope");
|
|
92
|
+
// 装配期过滤:这里只认已经 `apply` 过的会话;没登记过的(没装 session-mode 的部署)一律放行。
|
|
93
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
94
|
+
const agent = context.agent;
|
|
95
|
+
const applied = agent === undefined ? undefined : this.applied.get(agent);
|
|
96
|
+
if (applied === undefined) return next();
|
|
97
|
+
const allow = new Set(applied.mode.allowTools);
|
|
98
|
+
const result = await next();
|
|
99
|
+
return {
|
|
100
|
+
...result,
|
|
101
|
+
// 工具自己的说明 section 与工具目录同源:目录里没有的工具,它的说明也不该留在提示词里。
|
|
102
|
+
sections: result.sections.filter((section) =>
|
|
103
|
+
section.name.startsWith(TOOL_SECTION_PREFIX)
|
|
104
|
+
? allow.has(section.name.slice(TOOL_SECTION_PREFIX.length))
|
|
105
|
+
: true,
|
|
106
|
+
),
|
|
107
|
+
tools: result.tools.filter((tool) => allow.has(tool.name)),
|
|
108
|
+
};
|
|
98
109
|
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 把某个会话收口到这份定义上(同一个 agent 再调就是换一份)。
|
|
114
|
+
* @param agent - 目标会话的 agent。
|
|
115
|
+
* @param mode - 收口的全部输入。
|
|
116
|
+
*/
|
|
117
|
+
apply(agent: Agent, mode: SessionToolScopeMode): void {
|
|
118
|
+
const previous = this.applied.get(agent);
|
|
119
|
+
if (previous !== undefined) previous.dispose();
|
|
120
|
+
const allow = new Set(mode.allowTools);
|
|
121
|
+
|
|
122
|
+
// 落在 `agent.ctx` 上的那两件用一个 effect 装:抑制器是 scope 层的一次注册;执行层 guard 要等
|
|
123
|
+
// `tools` 激活才装得上(`inject` 的回调),它挂在那个 inject fiber 下,所以收回时连 fiber 一起收。
|
|
124
|
+
const dispose = agent.ctx.effect(() => {
|
|
125
|
+
const stoppers: (() => void)[] = [];
|
|
126
|
+
if (mode.runtimeContext === false) {
|
|
127
|
+
// service 在 `agent.ctx` 上是 shadow:抑制落在该 agent 的 scope 层,只覆盖这个会话。
|
|
128
|
+
stoppers.push(agent.ctx.systemPrompt.suppressRuntimeContext());
|
|
129
|
+
}
|
|
130
|
+
// 必须落在 `agent.ctx` 上才是这个会话的作用域。
|
|
131
|
+
const fiber = agent.ctx.inject(["tools"], (scope) => {
|
|
132
|
+
scope.tools.guard((exec) =>
|
|
133
|
+
allow.has(exec.name) ? undefined : OUT_OF_SCOPE(exec.name, mode.name),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
return [...stoppers, () => fiber.dispose()];
|
|
137
|
+
}, "sessionToolScope: per-session gate");
|
|
138
|
+
|
|
139
|
+
// 与"谁注册了这个工具"无关的那两件:通道按 agent 覆盖,不需要收回。
|
|
140
|
+
const channel = channelOf(this.ctx);
|
|
141
|
+
channel?.setInstructions(agent, mode.instructions !== false);
|
|
142
|
+
channel?.restrictTools(agent, (tool) => allow.has(tool));
|
|
143
|
+
|
|
144
|
+
this.applied.set(agent, { mode, dispose });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function apply(ctx: Context): void {
|
|
149
|
+
new SessionToolScope(ctx);
|
|
118
150
|
}
|
|
@@ -8,7 +8,7 @@ import { renderVirtualSkill } from "../assembler/index.ts";
|
|
|
8
8
|
|
|
9
9
|
export const name = "context-skill-catalog";
|
|
10
10
|
|
|
11
|
-
export const inject = ["skills", "tools", "contextAssembler"];
|
|
11
|
+
export const inject = ["agents", "skills", "tools", "contextAssembler"];
|
|
12
12
|
|
|
13
13
|
export const CATALOG_ID = "skill-catalog";
|
|
14
14
|
|
|
@@ -20,6 +20,13 @@ const DESCRIPTION_MAX_LENGTH = 500;
|
|
|
20
20
|
*
|
|
21
21
|
* 目录只列模型可调用的 skill;`auto` 的 skill 标 `modelInvocable: false`,所以它不出现在目录里
|
|
22
22
|
* (它的正文已经随提示送达)。工具的渲染用通道的虚拟 skill 形态,不带 `<skill_resources>`。
|
|
23
|
+
*
|
|
24
|
+
* **这一面归通道**(工作区指令那一面相反,是让位给 preset):官方 preset 自己装了上游 `tool-skill`,那份
|
|
25
|
+
* 注册在 preset 的 scope 层;我们把 `skill` 工具按会话注册进 **agent 自己那一层**——它最靠里,同名注册遮蔽
|
|
26
|
+
* 继承来的那一份([`core/tools/src/index.ts:1185-1207`](../../../vendor/deepseek-harness/packages/core/tools/src/index.ts)),
|
|
27
|
+
* 于是模型看到的是我们的工具(中文描述 + 按会话修剪的正文)。上游的目录发布判据是"它自己注册的那个工具是
|
|
28
|
+
* 本会话可见的那个"([`skill/tool-skill/src/index.ts:213-236`](../../../vendor/deepseek-harness/packages/skill/tool-skill/src/index.ts)),
|
|
29
|
+
* 被遮蔽之后它闭嘴,目录由我们发布;`minimal`(`chat`)那种没有上游行的会话里也只有我们这一份。
|
|
23
30
|
*/
|
|
24
31
|
export function apply(ctx: Context): void {
|
|
25
32
|
/**
|
|
@@ -28,12 +35,42 @@ export function apply(ctx: Context): void {
|
|
|
28
35
|
*/
|
|
29
36
|
const catalogEntries = new WeakMap<Agent, readonly { name: string; description: string }[]>();
|
|
30
37
|
|
|
38
|
+
// 同名工具只能有一个所有者:host 平面已经有人注册了 `skill`(装上游 `tool-skill` 的部署)就让给它,
|
|
39
|
+
// 我们只发布目录。我们的部署里 host 平面那一行是禁用的(preset 才装),所以走的是下面这条。
|
|
40
|
+
const skillTool = ctx.tools.get("skill") === undefined ? defineSkillTool(ctx) : undefined;
|
|
41
|
+
if (skillTool === undefined) {
|
|
42
|
+
ctx.logger.warn(
|
|
43
|
+
"context-skill-catalog: a `skill` tool is already registered; keeping the existing one and only publishing the catalog",
|
|
44
|
+
);
|
|
45
|
+
} else {
|
|
46
|
+
// 按会话注册:`register` 落到**调用它的那个 ctx** 的 scope 层(agent 自己那一层),随 agent 收回。
|
|
47
|
+
const install = (agent: Agent): void => {
|
|
48
|
+
// 本行重装(HMR / 设置面)时上次那份注册还在 agent 自己那一层(它随 agent 而不是随本行收回):
|
|
49
|
+
// 同一份定义就不要再注册一次——同一层重复注册会抛(`dsh-scope` 的 `NamedEntries.insert`)。
|
|
50
|
+
if (ctx.tools.get("skill", agent) === skillTool) return;
|
|
51
|
+
try {
|
|
52
|
+
agent.ctx.tools.register(skillTool);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
// agent 自己那一层被更具体的注册占了:让开(它自己会发目录),我们这一份注册不上。
|
|
55
|
+
ctx.logger.warn(`context-skill-catalog: ${String(error)}`);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
59
|
+
install(agent);
|
|
60
|
+
});
|
|
61
|
+
for (const agent of ctx.agents.list()) install(agent);
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
ctx.contextAssembler.registerRule({
|
|
32
65
|
id: CATALOG_ID,
|
|
33
|
-
//
|
|
34
|
-
//
|
|
66
|
+
// 对外身份**用我们自己的 kind**(不是上游那个 `skill-catalog`):目录的正文与条目都由我们发布,而上游
|
|
67
|
+
// `tool-skill` 的目录监听器把任何 `kind: 'skill-catalog'` 且条目可读的消息都当成**它自己的**账本
|
|
68
|
+
// (`catalogMessage` / `catalogHistory`),于是它会删掉我们这一条(首步:`!history.published &&
|
|
69
|
+
// skills.length === 0` 那条分支)或者补一条"没有可用 skill"的空目录把它顶掉(它有可见目录之后)。
|
|
70
|
+
// 形态仍是它认得的 `catalog`(`entries` 是客户端列条目的那份清单),只是 kind 归我们——
|
|
71
|
+
// 它的两个扫法都只看 kind,于是本会话里它彻底闭嘴。
|
|
35
72
|
source: (agent) => ({
|
|
36
|
-
kind: "
|
|
73
|
+
kind: "context-assembler",
|
|
37
74
|
form: "catalog",
|
|
38
75
|
entries: catalogEntries.get(agent) ?? [],
|
|
39
76
|
}),
|
|
@@ -79,21 +116,11 @@ export function apply(ctx: Context): void {
|
|
|
79
116
|
].join("\n");
|
|
80
117
|
},
|
|
81
118
|
});
|
|
82
|
-
|
|
83
|
-
// 同名工具只能有一个所有者:host 层的 `tool-skill` 行由 patch 禁用,但万一没禁掉(上游改了行 id、
|
|
84
|
-
// 或别的 bundle 又插了一份),重复注册会让 tools/change 抖动甚至让 session 创建失败——所以先看清
|
|
85
|
-
// 注册表里有没有,有就让给对方,我们只负责目录。
|
|
86
|
-
if (ctx.tools.get("skill") !== undefined) {
|
|
87
|
-
ctx.logger.warn(
|
|
88
|
-
"context-skill-catalog: a `skill` tool is already registered; keeping the existing one and only publishing the catalog",
|
|
89
|
-
);
|
|
90
|
-
} else {
|
|
91
|
-
registerSkillTool(ctx);
|
|
92
|
-
}
|
|
93
119
|
}
|
|
94
120
|
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
/** 一份工具定义,按会话注册进每个 agent 的自己那一层(同一个定义对象,判据里的同一性靠它)。 */
|
|
122
|
+
function defineSkillTool(ctx: Context): ReturnType<typeof defineTool> {
|
|
123
|
+
return defineTool({
|
|
97
124
|
name: "skill",
|
|
98
125
|
description: "按需加载 skill 的完整说明。",
|
|
99
126
|
parameters: {
|
|
@@ -169,7 +196,6 @@ function registerSkillTool(ctx: Context): void {
|
|
|
169
196
|
};
|
|
170
197
|
},
|
|
171
198
|
});
|
|
172
|
-
ctx.tools.register(skillTool);
|
|
173
199
|
}
|
|
174
200
|
|
|
175
201
|
function clamp(text: string, max: number): string {
|
package/cordis.patch.yml
DELETED
package/dist/scope-CI-fNq5A.mjs
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
-
import z from "@deepseek-ai/schemastery";
|
|
3
|
-
import "@deepseek-ai/cordis";
|
|
4
|
-
//#region ../../../vendor/deepseek-harness/packages/core/scope/lib/index.js
|
|
5
|
-
/**
|
|
6
|
-
* Scoped-context primitive: mint a Cordis context that tags registrations with
|
|
7
|
-
* an opaque identity and build routing-only event carriers for that identity.
|
|
8
|
-
*
|
|
9
|
-
* @module @deepseek-ai/dsh-scope
|
|
10
|
-
*/
|
|
11
|
-
/** Context tag written by {@link createScope}. */
|
|
12
|
-
const kScope = Symbol("dsh.scope");
|
|
13
|
-
/**
|
|
14
|
-
* The enclosing scope of each key. One relation powers both directions of
|
|
15
|
-
* scope nesting: registration views inherit DOWN the chain (a child scope
|
|
16
|
-
* sees its ancestors' layers — {@link ScopedLayers}), and event admission
|
|
17
|
-
* extends UP it (a listener tagged with an ancestor receives events dispatched
|
|
18
|
-
* to a descendant key — {@link scopeTarget}).
|
|
19
|
-
*/
|
|
20
|
-
const scopeParents = /* @__PURE__ */ new WeakMap();
|
|
21
|
-
/**
|
|
22
|
-
* The chain from a key to its root ancestor.
|
|
23
|
-
* @param key - the starting key, or `undefined` for the empty chain.
|
|
24
|
-
* @returns keys nearest-first: `[key, parent, grandparent, …]`.
|
|
25
|
-
*/
|
|
26
|
-
function scopeChainOf(key) {
|
|
27
|
-
const chain = [];
|
|
28
|
-
for (let cursor = key; cursor !== void 0; cursor = scopeParents.get(cursor)) chain.push(cursor);
|
|
29
|
-
return chain;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Read the nearest scope tag inherited by a context.
|
|
33
|
-
* @param ctx - context to inspect.
|
|
34
|
-
* @returns its scope key, or `undefined` for an unscoped context.
|
|
35
|
-
*/
|
|
36
|
-
function scopeOf(ctx) {
|
|
37
|
-
return ctx[kScope];
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
//#region src/scope/index.ts
|
|
41
|
-
var scope_exports = /* @__PURE__ */ __exportAll({
|
|
42
|
-
Config: () => Config,
|
|
43
|
-
apply: () => apply,
|
|
44
|
-
inject: () => inject,
|
|
45
|
-
name: () => name
|
|
46
|
-
});
|
|
47
|
-
const name = "context-scope";
|
|
48
|
-
const inject = ["tools", "systemPrompt"];
|
|
49
|
-
const Config = z.object({
|
|
50
|
-
allowTools: z.array(z.string()).default([]),
|
|
51
|
-
instructions: z.boolean().default(true),
|
|
52
|
-
runtimeContext: z.boolean().default(true)
|
|
53
|
-
});
|
|
54
|
-
const OUT_OF_SCOPE = (toolName) => `${toolName} 不在本模式的工具范围内,用它不会有结果;按当前模式提供的工具完成任务,或让用户切到别的模式。`;
|
|
55
|
-
/** 单个工具的说明 section 名前缀:`tool:<工具名>`(`tools:` 那类是聚合块,不归白名单管)。 */
|
|
56
|
-
const TOOL_SECTION_PREFIX = "tool:";
|
|
57
|
-
/**
|
|
58
|
-
* 把某个 preset 的工具收成"只有这些"。
|
|
59
|
-
*
|
|
60
|
-
* 为什么需要它:preset composition 只能决定"加什么",管不了 host 层——`dsh.profile.bundles` 打开的
|
|
61
|
-
* bundle(例如实验性的 Agent Teams)会在 host 层插工具行与 section,对所有 preset 一视同仁。模式想
|
|
62
|
-
* 表达"我只有这几个工具、一条提示词都不要"时,唯一与来源无关的做法是收口"有什么"。
|
|
63
|
-
*
|
|
64
|
-
* 为什么不是 `tools.restrict()`:它会改动可见工具集,从而触发 `tools/change`;上游 `tool-subagent`
|
|
65
|
-
* 正是用该事件做 composition reconcile——两边互相触发会变成装配风暴。这里用投影过滤加执行层 guard:
|
|
66
|
-
* 既不碰注册表、也不发事件。
|
|
67
|
-
*/
|
|
68
|
-
function apply(ctx, config) {
|
|
69
|
-
const allow = new Set(config.allowTools ?? []);
|
|
70
|
-
if (allow.size === 0) throw new Error("context-scope: `allowTools` must name at least one tool; omit the row instead of mounting an empty scope");
|
|
71
|
-
const presetKey = scopeOf(ctx);
|
|
72
|
-
const scoped = /* @__PURE__ */ new WeakSet();
|
|
73
|
-
if (config.runtimeContext === false) ctx.systemPrompt.suppressRuntimeContext();
|
|
74
|
-
const belongs = (agent) => {
|
|
75
|
-
if (presetKey === void 0) return true;
|
|
76
|
-
const key = scopeOf(agent.ctx);
|
|
77
|
-
return key !== void 0 && scopeChainOf(key).includes(presetKey);
|
|
78
|
-
};
|
|
79
|
-
/** 每个会话只做一次:拨提示词开关,并挂上执行层白名单。 */
|
|
80
|
-
const scopeOnce = (agent) => {
|
|
81
|
-
if (scoped.has(agent) || !belongs(agent)) return;
|
|
82
|
-
scoped.add(agent);
|
|
83
|
-
try {
|
|
84
|
-
const channel = ctx.get("contextAssembler");
|
|
85
|
-
if (config.instructions === false) channel?.setInstructions(agent, false);
|
|
86
|
-
channel?.restrictTools(agent, (tool) => allow.has(tool));
|
|
87
|
-
} catch {}
|
|
88
|
-
agent.ctx.inject(["tools"], () => {
|
|
89
|
-
agent.ctx.tools.guard((exec) => allow.has(exec.name) ? void 0 : OUT_OF_SCOPE(exec.name));
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
93
|
-
const agent = context.agent;
|
|
94
|
-
if (agent !== void 0) scopeOnce(agent);
|
|
95
|
-
const result = await next();
|
|
96
|
-
return {
|
|
97
|
-
...result,
|
|
98
|
-
sections: result.sections.filter((section) => section.name.startsWith(TOOL_SECTION_PREFIX) ? allow.has(section.name.slice(5)) : true),
|
|
99
|
-
tools: result.tools.filter((tool) => allow.has(tool.name))
|
|
100
|
-
};
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
//#endregion
|
|
104
|
-
export { scope_exports as a, name as i, apply as n, inject as r, Config as t };
|
package/tool/patch.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
|
|
5
|
-
import yaml from "js-yaml";
|
|
6
|
-
import { PATCH_ROWS } from "../src/rows.ts";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* bundle patch 的真源:把 {@link PATCH_ROWS}(`src/rows.ts` 那一份清单)写成 `cordis.patch.yml`。
|
|
10
|
-
*
|
|
11
|
-
* 本包有两种采用方式(见 `src/rows.ts`):`dsh.profile.bundles` 直接列出本包时用这份 patch;由
|
|
12
|
-
* preset 引用时走 preset 自己的行清单——**同一份真源**,不会两边漂移。
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
export const PATCH_FILE = "cordis.patch.yml";
|
|
16
|
-
|
|
17
|
-
const HEADER = `# 本文件由 packages/context/dsh-context-assembler/tool/patch.ts 生成,请勿手工编辑
|
|
18
|
-
`;
|
|
19
|
-
|
|
20
|
-
export function renderPatch(): string {
|
|
21
|
-
const body = yaml.dump([...PATCH_ROWS], {
|
|
22
|
-
schema: entryListSchema,
|
|
23
|
-
lineWidth: -1,
|
|
24
|
-
quotingType: '"',
|
|
25
|
-
});
|
|
26
|
-
return `${HEADER}${body}`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** 把 bundle patch 落到包根:它是发布产物的一部分(`files` 里有它,装配按出口解析)。 */
|
|
30
|
-
export async function generatePatch(): Promise<string> {
|
|
31
|
-
const path = join(resolve(dirname(fileURLToPath(import.meta.url)), ".."), PATCH_FILE);
|
|
32
|
-
await writeFile(path, renderPatch());
|
|
33
|
-
return path;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** tsdown 的 `build:done` 钩子:每次构建重写 `cordis.patch.yml`,入库的那份因此永远等于 `PATCH_ROWS`。 */
|
|
37
|
-
export function patchHooks(): { "build:done": () => Promise<void> } {
|
|
38
|
-
return {
|
|
39
|
-
"build:done": async () => {
|
|
40
|
-
process.stdout.write(`generated ${await generatePatch()}\n`);
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|