@morlay/dsh-context-assembler 0.0.1-alpha.3 → 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 CHANGED
@@ -26,12 +26,16 @@
26
26
  ——它是模式那一层的收口行,由 [`@morlay/dsh-session-mode`](../../profile/dsh-session-mode/README.md) 的 patch 与模式定义
27
27
  一起装(行 id 与 name 归本包的 `scopeRow()`,单一 home)。
28
28
 
29
+ 装一次不等于每次都注入:这两条面各自按**会话**判归谁——`agent-instructions` 在预设自带那一行时**让位**,
30
+ `skill-catalog` 反过来把同名工具注册进 agent 自己那一层**抢面**(见下面两节)。所以 `capabilities` 不在装配期
31
+ 按模式裁。
32
+
29
33
  工具说明(汉化 / 精简 / 用法分组)也不在这里:它归
30
34
  [`@morlay/dsh-agent-toolkit`](../../profile/dsh-agent-toolkit/README.md),同样在 profile 平面装一次。
31
35
 
32
36
  ## 装配形态
33
37
 
34
- 本包是 bundle:`cordis.patch.yml` 由 [`tool/patch.ts`](./tool/patch.ts) 从 [`src/rows.ts`](./src/rows.ts) 渲染,
38
+ 本包是 bundle:`cordis.patch.yml` 由 [`src/rows.ts`](./src/rows.ts) 从 [`src/rows.ts`](./src/rows.ts) 渲染,
35
39
  `rows` 出口导出同一份清单。`dsh.profile.bundles` 列出本包即装一次,**服务全局共享、不隔离**;模式的收口是
36
40
  另一行(`scopeRow()`:工具白名单 / instruction / 动态快照),别的包的行(工具说明、skill 目录)直接 `inject`
37
41
  同一份通道。
@@ -47,7 +51,7 @@
47
51
  全局通道)。本包只做上下文重排。
48
52
 
49
53
  引用展开不住在这里:它不依赖 `ctx.contextAssembler`(`inject` 只有 `skills`),也没有「读文件 + 字节预算」
50
- 以外与组装出口共享的东西,所以独立成包、由 [`@morlay/better-session`](../../session/better-session/cordis.patch.yml)
54
+ 以外与组装出口共享的东西,所以独立成包、由 [`@morlay/better-session`](../../bundles/better-session/cordis.patch.yml)
51
55
  装配(见 [ADR-引用展开拆成独立包](../../../.agents/adrs/20260923-引用展开拆成独立包并按profile装配.md))。
52
56
 
53
57
  **每个出口仍是独立的 cordis 插件**(各自的 `apply` 与 `inject`)——这一点是硬要求:合成单入口会让 `inject`
@@ -79,23 +83,42 @@ user 消息注入(每条按文本幂等,只有变化的那条重发)。
79
83
  `$DSH_HOME/AGENTS.md` 加项目根到 cwd 的逐级 `AGENTS.md` / `AGENTS.local.md`,**一条文件一条 id**
80
84
  (`agent-instructions:<根标识>:<文件>`,根标识是根目录的 8 位摘要——同进程两个项目根的同名文件因此不会互相
81
85
  顶掉),文件变化时只重发变了的那一份。取代上游 `@deepseek-ai/dsh-agent-instructions`(host 那行由上游
82
- web-app bundle 自己设在装配层;我们那份由本包的组装出口在 profile 平面装)。
86
+ web-app bundle 自己设在装配层;我们那份由本包的组装出口在 profile 平面装)——**在该会话的 preset 行清单没有
87
+ 上游那一行时**。
83
88
 
89
+ - **preset 自带那一行时让位**:官方 `standard` / `ptc` / `cordis` 的 preset 行清单里有
90
+ `@deepseek-ai/dsh-agent-instructions`(`minimal` 没有),装上的会话由 preset 侧注入,这一条一条都不注入;
91
+ `chat` 挂的 `minimal` 会话仍由我们提供。判据与理由见
92
+ [ADR-工作区指令让位skill面由通道抢面](./.agents/adrs/20260929-工作区指令让位skill面由通道抢面.md)。
84
93
  - **不跟踪 `read`/`write`/`edit`**:上游靠 touch 上浮触发刷新;本部署的 `AGENTS.md` 几乎不变,按
85
94
  `mtime:size` 对账足够。
86
95
  - **超预算可见**:单文件超过 `maxBytes` 时截断并留一行提示,不静默丢内容。
87
96
  - **与上游的 baseline 认领对齐**:`source` 带 `kind: "agent-instructions"` + `baseline: true` +
88
- `baselineIdentity`(身份与上游 `workspaceBaselineIdentity` 逐字相等)。少了后两样会出一个只在真会话里
89
- 看得见的毛病:**同会话切到另一套装配后出现两条 AGENTS.md**——上游认为基线不存在,再注入一条自己的模板
90
- ("Use them as guidance…"),两条口径矛盾且模型无法理解为覆盖。
97
+ `baselineIdentity`(身份与上游 `workspaceBaselineIdentity` 逐字相等)。上游的认领判据只看本步 claimed 的
98
+ 消息与会话 surface,所以它救不了首步两侧同时注入(那由让位管),但管得住另一件事:**我们注入过基线的历史
99
+ 会话在 preset 会话里恢复时**,上游认得出那条、不再补一份自己的模板("Use them as guidance…")。
91
100
  - 配置:`instructionFileCandidates` / `localInstructionFileCandidates`(覆盖成只读 `AGENTS*`,不含 CLAUDE
92
101
  系列)、`maxBytes`(65536)、`dshHome`。
93
102
 
94
103
  ## skill-catalog
95
104
 
96
105
  `skill-catalog` 规则块(`<available_skills>`,一行 `名字: 摘要`,只列模型可调用的 skill)加模型侧 `skill`
97
- 工具。取代上游 `@deepseek-ai/dsh-tool-skill`(`skill-filesystem` 保留——它提供 skill 发现)。目录变更走
98
- 同 id 覆盖;`auto` 的 skill 不进目录(正文已随提示送达)。
106
+ 工具。取代上游 `@deepseek-ai/dsh-tool-skill`(`skill-filesystem` 保留——它提供 skill 发现)。目录变更走同 id
107
+ 覆盖;`auto` 的 skill 不进目录(正文已随提示送达)。
108
+
109
+ **这一面归通道**(工作区指令那一面相反,是让位):`skill` 工具按**会话**注册进 agent 自己那一层,最近的一层
110
+ 赢,遮蔽 preset 那份;上游的目录发布判据是"它自己注册的那个工具是本会话可见的那个",被遮蔽之后它不发目录,
111
+ 目录由我们发布。要点:
112
+
113
+ - 目录那条注入消息的 **kind 是我们自己的**(`context-assembler` + `form: "catalog"` + `entries`)。上游把任何
114
+ `kind: "skill-catalog"` 且条目可读的消息当成它自己的账本:kind 用它的,它会在首步删掉我们这条、在有可见
115
+ 目录之后补一条"没有可用 skill"把目录顶掉(它文档里的 visibility-loss 语义)。`form` 与 `entries` 照旧,
116
+ 客户端那张条目表不受影响;注入行的标签变成 `context-assembler`。
117
+ - 按会话注册随 agent 收回;这一行重挂(HMR / 设置面)时不再重复注册同一份定义。
118
+ - `chat`(挂 `minimal`,没有上游那一行):工具与目录同样是我们提供的,但它的模式定义不收这两样
119
+ (`instructions: false`、白名单里没有 `skill`)——白名单把它挡在模型目录外,执行层 guard 拒绝调用。
120
+
121
+ 取舍与上游判据见 [ADR-工作区指令让位skill面由通道抢面](./.agents/adrs/20260929-工作区指令让位skill面由通道抢面.md)。
99
122
 
100
123
  ## 工具说明(不在这个包里)
101
124
 
@@ -106,7 +129,8 @@ web-app bundle 自己设在装配层;我们那份由本包的组装出口在 p
106
129
  ## scope
107
130
 
108
131
  按会话收口的出口(行 id `context-assembler-scope`):工具白名单(装配期投影 + 执行层 guard + 与工具同源的
109
- `tool:<工具名>` 说明 section,三侧同判据)、`instructions`(关掉规则块与降级 section)、`runtimeContext`
132
+ `tool:<工具名>` 说明 section,三侧同判据)、`instructions`(关掉一切 instruction 类送达:常驻组正文、规则块
133
+ 与降级 section;按需 skill 与 `@` 引用材料不受它管)、`runtimeContext`
110
134
  (关掉沙箱 / 审批那两条动态快照,按 scope 抑制)。
111
135
 
112
136
  **定义由消费方推给它**,它自己不认识"模式":`ctx.sessionToolScope.apply(agent, mode)` 是唯一入口,落在该
@@ -121,6 +121,40 @@ async function baselineIdentity(cwd) {
121
121
  });
122
122
  }
123
123
  //#endregion
124
+ //#region src/agent-instructions/preset-owner.ts
125
+ /** 上游那一行的模块名:与 `presets/*.patch.yml` 里写的一字不差。 */
126
+ const UPSTREAM_MODULE = "@deepseek-ai/dsh-agent-instructions";
127
+ /** 会话 → 它当前的 preset。preset 变了(空白窗口里换 preset)就整份重算。 */
128
+ const decided = /* @__PURE__ */ new WeakMap();
129
+ /**
130
+ * 该会话的 preset 自己提供了工作区指令这一面吗。
131
+ * @param ctx - 能解析到注册表的上下文(能力自己的 apply ctx)。
132
+ * @param agent - 要判的会话。
133
+ * @returns 行清单里有上游那一行且它是启用的。
134
+ */
135
+ function presetOwnsInstructions(ctx, agent) {
136
+ const registry = registryOf(ctx);
137
+ if (registry === void 0) return Promise.resolve(false);
138
+ const preset = registry.composedPreset(agent.ctx);
139
+ if (preset === void 0) return Promise.resolve(false);
140
+ const cached = decided.get(agent);
141
+ if (cached?.preset === preset) return cached.owns;
142
+ const owns = registry.compositionInventory().then((inventory) => inventory.some((composition) => composition.id === preset && composition.rows.some((row) => row.moduleName === UPSTREAM_MODULE && row.enabled === true))).catch(() => false);
143
+ decided.set(agent, {
144
+ preset,
145
+ owns
146
+ });
147
+ return owns;
148
+ }
149
+ /** 注册表可能压根没装(没有 preset 的部署形态);`ctx.get` 在不提供时读到 undefined。 */
150
+ function registryOf(ctx) {
151
+ try {
152
+ return ctx.get("agentPresets");
153
+ } catch {
154
+ return;
155
+ }
156
+ }
157
+ //#endregion
124
158
  //#region src/agent-instructions/index.ts
125
159
  var agent_instructions_exports = /* @__PURE__ */ __exportAll({
126
160
  Config: () => Config$1,
@@ -170,7 +204,8 @@ function apply(ctx, config) {
170
204
  baselineIdentity: identity,
171
205
  changes: []
172
206
  }),
173
- text: (target) => {
207
+ text: async (target) => {
208
+ if (await presetOwnsInstructions(ctx, target)) return "";
174
209
  const current = chains.get(target);
175
210
  if (current === void 0 || !current.files.some((entry) => entry.path === file.path)) return "";
176
211
  return readInstruction(file, cache, maxBytes);
@@ -1,2 +1,2 @@
1
- import { a as name, i as inject, r as apply, t as Config } from "./agent-instructions-BNEiZl0P.mjs";
1
+ import { a as name, i as inject, r as apply, t as Config } from "./agent-instructions-CIgUVIeo.mjs";
2
2
  export { Config, apply, inject, name };
@@ -13,7 +13,7 @@ const REMINDER_CLOSE = "</system-reminder>";
13
13
  const RULES_SECTION = "assembler:rules";
14
14
  /**
15
15
  * 这条消息是不是通道注入的条目——判据是 source 里的幂等键,而不是 kind:接管上游那两面的条目
16
- * 用上游 kind(`agent-instructions` / `skill-catalog`),幂等必须照样认。
16
+ * 接管工作区指令那一面的条目用上游 kind(`agent-instructions`),幂等必须照样认。
17
17
  */
18
18
  function promptEntryIdOf(source) {
19
19
  const id = source.id;
@@ -53,7 +53,7 @@ function latestReminderText(agent, id) {
53
53
  }
54
54
  /**
55
55
  * 注入一条提醒:`text` 是已渲染好的完整正文(规则块或内容块),`key` 是它的幂等键。
56
- * `source` 声明这条消息对外的身份——接管上游那两面用上游 kind,我们自己的条目留默认。
56
+ * `source` 声明这条消息对外的身份——工作区指令那一面用上游 kind,我们自己的条目留默认。
57
57
  */
58
58
  function reminderMessage(key, text, source) {
59
59
  return createUserMessage({
@@ -89,7 +89,10 @@ var ContextAssembler = class extends Service {
89
89
  suppressed = /* @__PURE__ */ new Set();
90
90
  skills = /* @__PURE__ */ new Map();
91
91
  states = /* @__PURE__ */ new WeakMap();
92
- /** 不要 instruction 类注入(规则块)的会话;内容块(技能正文、引用材料)不受它管。 */
92
+ /**
93
+ * 不要 instruction 类注入的会话(模式说了 `instructions: false`):常驻正文、规则块与降级 section 都不进。
94
+ * 按需 skill 不受它管(那是模型自己加载),引用材料也不经通道。
95
+ */
93
96
  withoutInstructions = /* @__PURE__ */ new WeakSet();
94
97
  /** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
95
98
  toolScopes = /* @__PURE__ */ new WeakMap();
@@ -137,7 +140,7 @@ var ContextAssembler = class extends Service {
137
140
  * 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
138
141
  * `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
139
142
  */
140
- /** 这个会话不要 instruction 类的规则块(模式说了 `instructions: false`)。 */
143
+ /** 这个会话要不要 instruction 类注入(模式说了 `instructions: false` 就是不要,见 {@link withoutInstructions})。 */
141
144
  setInstructions(agent, on) {
142
145
  if (on) this.withoutInstructions.delete(agent);
143
146
  else this.withoutInstructions.add(agent);
@@ -179,14 +182,23 @@ var ContextAssembler = class extends Service {
179
182
  contentFor(name, agent) {
180
183
  return this.declarations.get(name)?.content(agent);
181
184
  }
185
+ /** 一个 skill 在这个会话能不能被用到:声明了 `requires` 时至少一个工具在模式收窄内。 */
186
+ reachable(declaration, scope) {
187
+ const requires = declaration.requires;
188
+ if (requires === void 0 || requires.length === 0) return true;
189
+ if (scope === void 0) return true;
190
+ return requires.some((tool) => scope(tool));
191
+ }
182
192
  async collect(agent) {
183
193
  const entries = /* @__PURE__ */ new Map();
184
- for (const declaration of this.declarations.values()) {
194
+ const scope = this.toolScopes.get(agent);
195
+ const instructionsOff = this.withoutInstructions.has(agent);
196
+ if (!instructionsOff) for (const declaration of this.declarations.values()) {
185
197
  if ((declaration.injection ?? "on-demand") !== "auto") continue;
198
+ if (!this.reachable(declaration, scope)) continue;
186
199
  const body = declaration.content(agent);
187
200
  if (body.length > 0) entries.set(declaration.name, { text: renderVirtualSkill(declaration.name, body) });
188
201
  }
189
- const instructionsOff = this.withoutInstructions.has(agent);
190
202
  for (const declaration of this.rules.values()) {
191
203
  if (instructionsOff) break;
192
204
  const text = await withTimeout(declaration.text(agent), CONTENT_TIMEOUT_MS);
@@ -1,2 +1,2 @@
1
- import { a as name, c as InjectionMode, d as PromptSkillDeclaration, f as DEFAULT_KEEP, i as inject, l as PromptEntry, m as DEFAULT_SUPPRESS, n as apply, o as renderVirtualSkill, p as DEFAULT_REPLACE, s as ContextAssembler, t as Config, u as PromptRuleDeclaration } from "./index-BGkeEaQA.mjs";
1
+ import { a as name, c as InjectionMode, d as PromptSkillDeclaration, f as DEFAULT_KEEP, i as inject, l as PromptEntry, m as DEFAULT_SUPPRESS, n as apply, o as renderVirtualSkill, p as DEFAULT_REPLACE, s as ContextAssembler, t as Config, u as PromptRuleDeclaration } from "./index-zhmD45A9.mjs";
2
2
  export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, type InjectionMode, type PromptEntry, type PromptRuleDeclaration, type PromptSkillDeclaration, apply, inject, name, renderVirtualSkill };
@@ -1,2 +1,2 @@
1
- import { a as name, c as DEFAULT_SUPPRESS, i as inject, l as ContextAssembler, n as apply, o as DEFAULT_KEEP, s as DEFAULT_REPLACE, t as Config, u as renderVirtualSkill } from "./assembler-_F9urbPj.mjs";
1
+ import { a as name, c as DEFAULT_SUPPRESS, i as inject, l as ContextAssembler, n as apply, o as DEFAULT_KEEP, s as DEFAULT_REPLACE, t as Config, u as renderVirtualSkill } from "./assembler-CDMIBAi8.mjs";
2
2
  export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, apply, inject, name, renderVirtualSkill };
@@ -68,7 +68,7 @@ interface PromptRuleDeclaration {
68
68
  readonly id: string;
69
69
  readonly text: (agent: Agent) => string | Promise<string>;
70
70
  /**
71
- * 这条规则的对外身份。接管上游那两面(工作区指令 / skill 目录)按上游 kind 发消息,好让按 kind 认领的
71
+ * 这条规则的对外身份。工作区指令那一面按上游 kind 发消息(skill 面相反,用我们自己的 kind),好让按 kind 认领的
72
72
  * 消费方(客户端标签、上游的实验性约束收集)认得出来;不声明就是通道自己的 `context-assembler`。
73
73
  * 幂等键仍是 `id`(`reminderMessage` 会把它塞进 source)。
74
74
  */
@@ -96,7 +96,10 @@ declare class ContextAssembler extends Service {
96
96
  private readonly suppressed;
97
97
  private readonly skills;
98
98
  private readonly states;
99
- /** 不要 instruction 类注入(规则块)的会话;内容块(技能正文、引用材料)不受它管。 */
99
+ /**
100
+ * 不要 instruction 类注入的会话(模式说了 `instructions: false`):常驻正文、规则块与降级 section 都不进。
101
+ * 按需 skill 不受它管(那是模型自己加载),引用材料也不经通道。
102
+ */
100
103
  private readonly withoutInstructions;
101
104
  /** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
102
105
  private readonly toolScopes;
@@ -113,7 +116,7 @@ declare class ContextAssembler extends Service {
113
116
  * 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
114
117
  * `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
115
118
  */
116
- /** 这个会话不要 instruction 类的规则块(模式说了 `instructions: false`)。 */
119
+ /** 这个会话要不要 instruction 类注入(模式说了 `instructions: false` 就是不要,见 {@link withoutInstructions})。 */
117
120
  setInstructions(agent: Agent, on: boolean): void;
118
121
  /**
119
122
  * 本会话的**工具收窄**(模式白名单):投影层只过滤装配结果、不碰工具注册表,所以"谁能用"这件事
@@ -137,6 +140,8 @@ declare class ContextAssembler extends Service {
137
140
  * 路径(接管的 `skill` 工具)要用这里重新算一次,才和 `auto` 正文一样跟着工具走。
138
141
  */
139
142
  contentFor(name: string, agent: Agent | undefined): string | undefined;
143
+ /** 一个 skill 在这个会话能不能被用到:声明了 `requires` 时至少一个工具在模式收窄内。 */
144
+ private reachable;
140
145
  collect(agent: Agent): Promise<Map<string, PromptEntry>>;
141
146
  private sectionsOf;
142
147
  /**
@@ -159,7 +164,19 @@ declare module "@deepseek-ai/cordis" {
159
164
  /** 通道自己的条目形态(注入方没声明 source 时的默认):规则块,幂等键是 id。 */
160
165
  interface PromptReminderSource {
161
166
  kind: "context-assembler";
162
- form: "instructions";
167
+ /**
168
+ * 条目形态:`instructions` 是规则块;`catalog` 是"带清单的目录"(客户端按 `entries` 列条目,
169
+ * 而不是读正文)。
170
+ */
171
+ form: "instructions" | "catalog";
172
+ /**
173
+ * `catalog` 形态发布的那份清单。**只有我们自己发布的目录**用它:上游 `tool-skill` 的账本扫法
174
+ * 只认它自己的 `kind`,所以这里的 kind 必须是我们自己的(见 [`skill-catalog`](../skill-catalog/index.ts))。
175
+ */
176
+ entries?: readonly {
177
+ readonly name: string;
178
+ readonly description: string;
179
+ }[];
163
180
  /** 条目 id:同 id 的最新一条取代更早的同 id 条目。本插件早先落库的消息没有它。 */
164
181
  id?: string;
165
182
  }
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as index_d_exports } from "./agent-instructions.mjs";
2
- import { r as index_d_exports$1 } from "./index-BGkeEaQA.mjs";
2
+ import { r as index_d_exports$1 } from "./index-zhmD45A9.mjs";
3
3
  import { t as index_d_exports$2 } from "./scope.mjs";
4
4
  import { t as index_d_exports$3 } from "./skill-catalog.mjs";
5
5
  import z from "@deepseek-ai/schemastery";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { n as agent_instructions_exports } from "./agent-instructions-BNEiZl0P.mjs";
2
- import { r as assembler_exports } from "./assembler-_F9urbPj.mjs";
1
+ import { n as agent_instructions_exports } from "./agent-instructions-CIgUVIeo.mjs";
2
+ import { r as assembler_exports } from "./assembler-CDMIBAi8.mjs";
3
3
  import { t as scope_exports } from "./scope.mjs";
4
4
  import { t as skill_catalog_exports } from "./skill-catalog.mjs";
5
5
  import z from "@deepseek-ai/schemastery";
@@ -10,6 +10,13 @@ declare const CATALOG_ID = "skill-catalog";
10
10
  *
11
11
  * 目录只列模型可调用的 skill;`auto` 的 skill 标 `modelInvocable: false`,所以它不出现在目录里
12
12
  * (它的正文已经随提示送达)。工具的渲染用通道的虚拟 skill 形态,不带 `<skill_resources>`。
13
+ *
14
+ * **这一面归通道**(工作区指令那一面相反,是让位给 preset):官方 preset 自己装了上游 `tool-skill`,那份
15
+ * 注册在 preset 的 scope 层;我们把 `skill` 工具按会话注册进 **agent 自己那一层**——它最靠里,同名注册遮蔽
16
+ * 继承来的那一份([`core/tools/src/index.ts:1185-1207`](../../../vendor/deepseek-harness/packages/core/tools/src/index.ts)),
17
+ * 于是模型看到的是我们的工具(中文描述 + 按会话修剪的正文)。上游的目录发布判据是"它自己注册的那个工具是
18
+ * 本会话可见的那个"([`skill/tool-skill/src/index.ts:213-236`](../../../vendor/deepseek-harness/packages/skill/tool-skill/src/index.ts)),
19
+ * 被遮蔽之后它闭嘴,目录由我们发布;`minimal`(`chat`)那种没有上游行的会话里也只有我们这一份。
13
20
  */
14
21
  declare function apply(ctx: Context): void;
15
22
  //#endregion
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import { u as renderVirtualSkill } from "./assembler-_F9urbPj.mjs";
2
+ import { u as renderVirtualSkill } from "./assembler-CDMIBAi8.mjs";
3
3
  import { isModelInvocable, isSkillName } from "@deepseek-ai/dsh-skill";
4
4
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
5
  //#region src/skill-catalog/index.ts
@@ -11,6 +11,7 @@ var skill_catalog_exports = /* @__PURE__ */ __exportAll({
11
11
  });
12
12
  const name = "context-skill-catalog";
13
13
  const inject = [
14
+ "agents",
14
15
  "skills",
15
16
  "tools",
16
17
  "contextAssembler"
@@ -23,6 +24,13 @@ const DESCRIPTION_MAX_LENGTH = 500;
23
24
  *
24
25
  * 目录只列模型可调用的 skill;`auto` 的 skill 标 `modelInvocable: false`,所以它不出现在目录里
25
26
  * (它的正文已经随提示送达)。工具的渲染用通道的虚拟 skill 形态,不带 `<skill_resources>`。
27
+ *
28
+ * **这一面归通道**(工作区指令那一面相反,是让位给 preset):官方 preset 自己装了上游 `tool-skill`,那份
29
+ * 注册在 preset 的 scope 层;我们把 `skill` 工具按会话注册进 **agent 自己那一层**——它最靠里,同名注册遮蔽
30
+ * 继承来的那一份([`core/tools/src/index.ts:1185-1207`](../../../vendor/deepseek-harness/packages/core/tools/src/index.ts)),
31
+ * 于是模型看到的是我们的工具(中文描述 + 按会话修剪的正文)。上游的目录发布判据是"它自己注册的那个工具是
32
+ * 本会话可见的那个"([`skill/tool-skill/src/index.ts:213-236`](../../../vendor/deepseek-harness/packages/skill/tool-skill/src/index.ts)),
33
+ * 被遮蔽之后它闭嘴,目录由我们发布;`minimal`(`chat`)那种没有上游行的会话里也只有我们这一份。
26
34
  */
27
35
  function apply(ctx) {
28
36
  /**
@@ -30,10 +38,26 @@ function apply(ctx) {
30
38
  * (`collect` 里先算正文再取 source)。
31
39
  */
32
40
  const catalogEntries = /* @__PURE__ */ new WeakMap();
41
+ const skillTool = ctx.tools.get("skill") === void 0 ? defineSkillTool(ctx) : void 0;
42
+ if (skillTool === void 0) ctx.logger.warn("context-skill-catalog: a `skill` tool is already registered; keeping the existing one and only publishing the catalog");
43
+ else {
44
+ const install = (agent) => {
45
+ if (ctx.tools.get("skill", agent) === skillTool) return;
46
+ try {
47
+ agent.ctx.tools.register(skillTool);
48
+ } catch (error) {
49
+ ctx.logger.warn(`context-skill-catalog: ${String(error)}`);
50
+ }
51
+ };
52
+ ctx.on("agent/created", ({ agent }) => {
53
+ install(agent);
54
+ });
55
+ for (const agent of ctx.agents.list()) install(agent);
56
+ }
33
57
  ctx.contextAssembler.registerRule({
34
58
  id: CATALOG_ID,
35
59
  source: (agent) => ({
36
- kind: "skill-catalog",
60
+ kind: "context-assembler",
37
61
  form: "catalog",
38
62
  entries: catalogEntries.get(agent) ?? []
39
63
  }),
@@ -66,11 +90,10 @@ function apply(ctx) {
66
90
  ].join("\n");
67
91
  }
68
92
  });
69
- if (ctx.tools.get("skill") !== void 0) ctx.logger.warn("context-skill-catalog: a `skill` tool is already registered; keeping the existing one and only publishing the catalog");
70
- else registerSkillTool(ctx);
71
93
  }
72
- function registerSkillTool(ctx) {
73
- const skillTool = defineTool({
94
+ /** 一份工具定义,按会话注册进每个 agent 的自己那一层(同一个定义对象,判据里的同一性靠它)。 */
95
+ function defineSkillTool(ctx) {
96
+ return defineTool({
74
97
  name: "skill",
75
98
  description: "按需加载 skill 的完整说明。",
76
99
  parameters: { name: {
@@ -175,7 +198,6 @@ function registerSkillTool(ctx) {
175
198
  };
176
199
  }
177
200
  });
178
- ctx.tools.register(skillTool);
179
201
  }
180
202
  function clamp(text, max) {
181
203
  return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morlay/dsh-context-assembler",
3
- "version": "0.0.1-alpha.3",
3
+ "version": "0.0.1-alpha.4",
4
4
  "description": "Prompt-injection capability set for dsh: the single channel (context assembler) plus the injectors it carries — workspace instructions, skill catalog — plus the scope gate that narrows tools per session mode. Reference expansion is its own package (@morlay/dsh-reference).",
5
5
  "keywords": [
6
6
  "context",
@@ -19,65 +19,63 @@
19
19
  "dist",
20
20
  "src",
21
21
  "tool",
22
- "cordis.patch.yml",
23
22
  "!**/__tests__"
24
23
  ],
25
24
  "type": "module",
26
25
  "exports": {
27
26
  ".": "./dist/index.mjs",
27
+ "./rows": "./dist/rows.mjs",
28
28
  "./assembler": "./dist/assembler.mjs",
29
29
  "./agent-instructions": "./dist/agent-instructions.mjs",
30
30
  "./skill-catalog": "./dist/skill-catalog.mjs",
31
31
  "./scope": "./dist/scope.mjs",
32
32
  "./package.json": "./package.json",
33
- "./rows": "./dist/rows.mjs",
34
- "./cordis.patch.yml": "./cordis.patch.yml",
35
33
  "./locale/*.json": "./locale/*.json"
36
34
  },
37
35
  "dependencies": {
38
- "@deepseek-ai/schemastery": "3.18.4"
36
+ "@deepseek-ai/schemastery": "^3.18.4"
39
37
  },
40
38
  "devDependencies": {
41
39
  "@deepseek-ai/cordis": "4.0.4",
40
+ "@deepseek-ai/cordis-plugin-group": "1.0.4",
42
41
  "@deepseek-ai/cordis-plugin-include": "1.0.9",
43
- "@deepseek-ai/dsh-agent": "0.1.7-rc.1",
44
- "@deepseek-ai/dsh-agent-instructions": "0.1.7-rc.1",
45
- "@deepseek-ai/dsh-agent-loop": "0.1.7-rc.1",
46
- "@deepseek-ai/dsh-agent-loop-testkit": "0.1.7-rc.1",
47
- "@deepseek-ai/dsh-commands": "0.1.7-rc.1",
48
- "@deepseek-ai/dsh-compaction-basic": "0.1.7-rc.1",
49
- "@deepseek-ai/dsh-fs": "0.1.7-rc.1",
50
- "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
51
- "@deepseek-ai/dsh-scope": "0.1.7-rc.1",
52
- "@deepseek-ai/dsh-session": "0.1.7-rc.1",
53
- "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.7-rc.1",
54
- "@deepseek-ai/dsh-skill": "0.1.7-rc.1",
55
- "@deepseek-ai/dsh-skill-filesystem": "0.1.7-rc.1",
56
- "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
57
- "@deepseek-ai/dsh-token-meter": "0.1.7-rc.1",
58
- "@deepseek-ai/dsh-tool-skill": "0.1.7-rc.1",
59
- "@deepseek-ai/dsh-tools": "0.1.7-rc.1",
60
- "@morlay/session-rdb": "0.0.21-alpha.8",
42
+ "@deepseek-ai/cordis-plugin-loader": "1.0.5",
43
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
44
+ "@deepseek-ai/dsh-agent-instructions": "0.1.7-rc.2",
45
+ "@deepseek-ai/dsh-agent-loop": "0.1.7-rc.2",
46
+ "@deepseek-ai/dsh-agent-loop-testkit": "0.1.7-rc.2",
47
+ "@deepseek-ai/dsh-agent-preset-registry": "0.1.7-rc.2",
48
+ "@deepseek-ai/dsh-commands": "0.1.7-rc.2",
49
+ "@deepseek-ai/dsh-compaction-basic": "0.1.7-rc.2",
50
+ "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
51
+ "@deepseek-ai/dsh-fs-local": "0.1.7-rc.2",
52
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
53
+ "@deepseek-ai/dsh-scope": "0.1.7-rc.2",
54
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
55
+ "@deepseek-ai/dsh-session-persistence-jsonl": "0.1.7-rc.2",
56
+ "@deepseek-ai/dsh-skill": "0.1.7-rc.2",
57
+ "@deepseek-ai/dsh-skill-filesystem": "0.1.7-rc.2",
58
+ "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.2",
59
+ "@deepseek-ai/dsh-token-meter": "0.1.7-rc.2",
60
+ "@deepseek-ai/dsh-tool-skill": "0.1.7-rc.2",
61
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
62
+ "@morlay/session-rdb": "0.0.21-alpha.9",
61
63
  "@types/js-yaml": "^4.0.9",
62
64
  "@types/mdast": "^4.0.4",
63
65
  "js-yaml": "^4.1.0"
64
66
  },
65
67
  "peerDependencies": {
66
- "@deepseek-ai/cordis": "4.0.4",
67
- "@deepseek-ai/dsh-agent": "0.1.7-rc.1",
68
- "@deepseek-ai/dsh-agent-instructions": "0.1.7-rc.1",
69
- "@deepseek-ai/dsh-commands": "0.1.7-rc.1",
70
- "@deepseek-ai/dsh-fs": "0.1.7-rc.1",
71
- "@deepseek-ai/dsh-llm": "0.1.7-rc.1",
72
- "@deepseek-ai/dsh-session": "0.1.7-rc.1",
73
- "@deepseek-ai/dsh-skill": "0.1.7-rc.1",
74
- "@deepseek-ai/dsh-system-prompt": "0.1.7-rc.1",
75
- "@deepseek-ai/dsh-tools": "0.1.7-rc.1"
76
- },
77
- "dsh": {
78
- "bundle": {
79
- "patch": "./cordis.patch.yml"
80
- }
68
+ "@deepseek-ai/cordis": "^4.0.4",
69
+ "@deepseek-ai/dsh-agent": "^0.1.7-rc.2",
70
+ "@deepseek-ai/dsh-agent-instructions": "^0.1.7-rc.2",
71
+ "@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.2",
72
+ "@deepseek-ai/dsh-commands": "^0.1.7-rc.2",
73
+ "@deepseek-ai/dsh-fs": "^0.1.7-rc.2",
74
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
75
+ "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
76
+ "@deepseek-ai/dsh-skill": "^0.1.7-rc.2",
77
+ "@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.2",
78
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.2"
81
79
  },
82
80
  "scripts": {
83
81
  "build": "pnpm exec tsdown"
@@ -7,6 +7,7 @@ import z from "@deepseek-ai/schemastery";
7
7
  import type {} from "../assembler/index.ts";
8
8
  import { instructionChain, readInstruction, type InstructionFile } from "./files.ts";
9
9
  import { baselineIdentity } from "./baseline.ts";
10
+ import { presetOwnsInstructions } from "./preset-owner.ts";
10
11
 
11
12
  export const name = "context-agent-instructions";
12
13
 
@@ -59,7 +60,7 @@ export function apply(ctx: Context, config: Config): void {
59
60
  for (const file of chain.files) {
60
61
  ctx.contextAssembler.registerRule({
61
62
  id: `agent-instructions:${rootTag(file.root)}:${file.display}`,
62
- // 对外身份沿用上游那两样:kind 让客户端标签与按 kind 认领的消费方(上游的实验性约束收集)认得这是
63
+ // 对外身份沿用上游那两样(skill 面相反,用我们自己的 kind):kind 让客户端标签与按 kind 认领的消费方认得这是
63
64
  // 工作区指令;`baseline` 与 `baselineIdentity` 让上游的认领判据(kind + baseline===true + 身份相等)
64
65
  // 认这份条目就是基线,于是它不再注入自己那条模板。`changes` 留空——上游那套按文件做 reconciliation
65
66
  // 的记录我们不做(一条文件一条 id)。
@@ -70,7 +71,10 @@ export function apply(ctx: Context, config: Config): void {
70
71
  baselineIdentity: identity,
71
72
  changes: [],
72
73
  }),
73
- text: (target) => {
74
+ text: async (target) => {
75
+ // 这个会话的 preset 自己装了这一面(官方 `standard` / `ptc` / `cordis` 都装了上游那行)就让位:
76
+ // 同一份工作区指令只从一处到模型(见 [`preset-owner.ts`](./preset-owner.ts))。
77
+ if (await presetOwnsInstructions(ctx, target)) return "";
74
78
  const current = chains.get(target);
75
79
  if (current === undefined || !current.files.some((entry) => entry.path === file.path))
76
80
  return "";
@@ -0,0 +1,70 @@
1
+ /**
2
+ * 「这个会话的 preset 行清单里装了上游 `agent-instructions` 吗」——装了就让位,这一面归 preset。
3
+ *
4
+ * 为什么要有这条判据:官方 web-app 把 `agent-instructions` 从 host 平面设成 `disabled`、交给 preset 的行清单
5
+ * ([`web-app/cordis.patch.yml:546`](../../../vendor/deepseek-harness/packages/bundle/web-app/cordis.patch.yml) 与
6
+ * `presets/*.patch.yml`)。本部署保留官方 preset 整套,于是**同一个会话**里 preset 平面的上游行与 host 平面的
7
+ * 我们都会注入:一步里两条 AGENTS.md,而模型无法把后一条理解成"作废前一条"(两条的幂等键不同)。
8
+ *
9
+ * 方向:**我们让位**,不是上游认领我们。上游 `agent-instructions` 的认领判据只看它**看得见**的东西
10
+ * (本步 claimed 的消息 + 会话 surface:
11
+ * [`agent-instructions/src/index.ts:46-63`](../../../vendor/deepseek-harness/packages/context/agent-instructions/src/index.ts)),
12
+ * 第 1 步两侧都在各自算注入,谁也算不出对方那一条,所以"由上游认领"救不了首步的重复
13
+ * (`baselineIdentity` 那份身份管的是另一件事,见 [`baseline.ts`](./baseline.ts))。
14
+ *
15
+ * 判据取**行清单里的模块名**而不是行 id:行 id 在 preset 的 `config.plugins` 里是自由的,模块名才是"哪一面的
16
+ * 提供者"。`enabled === true` 表示这一行在这个代际真的装着;`'conditional'`(`!!js` 读不出)按"没装"处理——
17
+ * 读不出就不敢让位,宁可我们提供。
18
+ */
19
+ import type { Context } from "@deepseek-ai/cordis";
20
+ import type { Agent } from "@deepseek-ai/dsh-agent";
21
+ // 只用它的类型:`ctx.agentPresets` 的形状来自上游注册表的声明合并,装不上时这里读到 undefined。
22
+ import type {} from "@deepseek-ai/dsh-agent-preset-registry";
23
+
24
+ /** 上游那一行的模块名:与 `presets/*.patch.yml` 里写的一字不差。 */
25
+ const UPSTREAM_MODULE = "@deepseek-ai/dsh-agent-instructions";
26
+
27
+ /** 会话 → 它当前的 preset。preset 变了(空白窗口里换 preset)就整份重算。 */
28
+ const decided = new WeakMap<Agent, { preset: string; owns: Promise<boolean> }>();
29
+
30
+ /**
31
+ * 该会话的 preset 自己提供了工作区指令这一面吗。
32
+ * @param ctx - 能解析到注册表的上下文(能力自己的 apply ctx)。
33
+ * @param agent - 要判的会话。
34
+ * @returns 行清单里有上游那一行且它是启用的。
35
+ */
36
+ export function presetOwnsInstructions(ctx: Context, agent: Agent): Promise<boolean> {
37
+ const registry = registryOf(ctx);
38
+ if (registry === undefined) return Promise.resolve(false);
39
+ const preset = registry.composedPreset(agent.ctx);
40
+ // 没挂 preset(子代理还没 join、没装注册表的部署、测试里的假 standing scope):这一面归我们。
41
+ if (preset === undefined) return Promise.resolve(false);
42
+
43
+ const cached = decided.get(agent);
44
+ if (cached?.preset === preset) return cached.owns;
45
+
46
+ const owns = registry
47
+ .compositionInventory()
48
+ .then((inventory) =>
49
+ inventory.some(
50
+ (composition) =>
51
+ composition.id === preset &&
52
+ composition.rows.some(
53
+ (row) => row.moduleName === UPSTREAM_MODULE && row.enabled === true,
54
+ ),
55
+ ),
56
+ )
57
+ // 读不出来就当我们提供:缺内容比重复更糟,而"读不出来"只可能来自坏掉的注册表。
58
+ .catch(() => false);
59
+ decided.set(agent, { preset, owns });
60
+ return owns;
61
+ }
62
+
63
+ /** 注册表可能压根没装(没有 preset 的部署形态);`ctx.get` 在不提供时读到 undefined。 */
64
+ function registryOf(ctx: Context): Context["agentPresets"] | undefined {
65
+ try {
66
+ return ctx.get("agentPresets") as Context["agentPresets"] | undefined;
67
+ } catch {
68
+ return undefined;
69
+ }
70
+ }
@@ -52,7 +52,7 @@ export interface PromptRuleDeclaration {
52
52
  readonly id: string;
53
53
  readonly text: (agent: Agent) => string | Promise<string>;
54
54
  /**
55
- * 这条规则的对外身份。接管上游那两面(工作区指令 / skill 目录)按上游 kind 发消息,好让按 kind 认领的
55
+ * 这条规则的对外身份。工作区指令那一面按上游 kind 发消息(skill 面相反,用我们自己的 kind),好让按 kind 认领的
56
56
  * 消费方(客户端标签、上游的实验性约束收集)认得出来;不声明就是通道自己的 `context-assembler`。
57
57
  * 幂等键仍是 `id`(`reminderMessage` 会把它塞进 source)。
58
58
  */
@@ -81,7 +81,10 @@ export class ContextAssembler extends Service {
81
81
  private readonly suppressed = new Set<string>();
82
82
  private readonly skills = new Map<string, RegisteredSkill>();
83
83
  private readonly states = new WeakMap<Agent, AgentState>();
84
- /** 不要 instruction 类注入(规则块)的会话;内容块(技能正文、引用材料)不受它管。 */
84
+ /**
85
+ * 不要 instruction 类注入的会话(模式说了 `instructions: false`):常驻正文、规则块与降级 section 都不进。
86
+ * 按需 skill 不受它管(那是模型自己加载),引用材料也不经通道。
87
+ */
85
88
  private readonly withoutInstructions = new WeakSet<Agent>();
86
89
 
87
90
  /** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
@@ -137,7 +140,7 @@ export class ContextAssembler extends Service {
137
140
  * 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
138
141
  * `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
139
142
  */
140
- /** 这个会话不要 instruction 类的规则块(模式说了 `instructions: false`)。 */
143
+ /** 这个会话要不要 instruction 类注入(模式说了 `instructions: false` 就是不要,见 {@link withoutInstructions})。 */
141
144
  setInstructions(agent: Agent, on: boolean): void {
142
145
  if (on) this.withoutInstructions.delete(agent);
143
146
  else this.withoutInstructions.add(agent);
@@ -184,16 +187,37 @@ export class ContextAssembler extends Service {
184
187
  return this.declarations.get(name)?.content(agent);
185
188
  }
186
189
 
190
+ /** 一个 skill 在这个会话能不能被用到:声明了 `requires` 时至少一个工具在模式收窄内。 */
191
+ private reachable(
192
+ declaration: PromptSkillDeclaration,
193
+ scope: ((tool: string) => boolean) | undefined,
194
+ ): boolean {
195
+ const requires = declaration.requires;
196
+ if (requires === undefined || requires.length === 0) return true;
197
+ // 没有收窄(没装模式的部署)就是全可见。
198
+ if (scope === undefined) return true;
199
+ return requires.some((tool) => scope(tool));
200
+ }
201
+
187
202
  async collect(agent: Agent): Promise<Map<string, PromptEntry>> {
188
203
  const entries = new Map<string, PromptEntry>();
189
- for (const declaration of this.declarations.values()) {
190
- if ((declaration.injection ?? "on-demand") !== "auto") continue;
191
- const body = declaration.content(agent);
192
- // 常驻送达的 skill 正文与按需加载同一形态:内容块,不是规则块。
193
- if (body.length > 0)
194
- entries.set(declaration.name, { text: renderVirtualSkill(declaration.name, body) });
195
- }
204
+ const scope = this.toolScopes.get(agent);
205
+ // 这一档开关管**一切 instruction 类送达**:常驻正文(内容块)、规则块、降级 section。按需 skill 照常注册
206
+ // (模型自己调 `skill` 才拿到正文,那已不是"注入");用户手打 `@` 引用带进来的材料走
207
+ // `@morlay/dsh-reference` 的 pre-step,不经这里。
196
208
  const instructionsOff = this.withoutInstructions.has(agent);
209
+ if (!instructionsOff) {
210
+ for (const declaration of this.declarations.values()) {
211
+ if ((declaration.injection ?? "on-demand") !== "auto") continue;
212
+ // 常驻正文也跟随工具可见性:它声明的 `requires` 工具在这个会话一个都不可用,正文就不该注入
213
+ // (与 skill 目录那侧的 `hiddenSkills` 同一判据;通道不碰 tools 注册表,只用模式收窄那一份)。
214
+ if (!this.reachable(declaration, scope)) continue;
215
+ const body = declaration.content(agent);
216
+ // 常驻送达的 skill 正文与按需加载同一形态:内容块,不是规则块。
217
+ if (body.length > 0)
218
+ entries.set(declaration.name, { text: renderVirtualSkill(declaration.name, body) });
219
+ }
220
+ }
197
221
  for (const declaration of this.rules.values()) {
198
222
  if (instructionsOff) break;
199
223
  // 注入路径不能被任何一个内容提供者拖死:文本是异步算的(技能目录要读注册表、工作区指令要读文件),
@@ -14,7 +14,16 @@ export const RULES_SECTION = "assembler:rules";
14
14
  /** 通道自己的条目形态(注入方没声明 source 时的默认):规则块,幂等键是 id。 */
15
15
  export interface PromptReminderSource {
16
16
  kind: "context-assembler";
17
- form: "instructions";
17
+ /**
18
+ * 条目形态:`instructions` 是规则块;`catalog` 是"带清单的目录"(客户端按 `entries` 列条目,
19
+ * 而不是读正文)。
20
+ */
21
+ form: "instructions" | "catalog";
22
+ /**
23
+ * `catalog` 形态发布的那份清单。**只有我们自己发布的目录**用它:上游 `tool-skill` 的账本扫法
24
+ * 只认它自己的 `kind`,所以这里的 kind 必须是我们自己的(见 [`skill-catalog`](../skill-catalog/index.ts))。
25
+ */
26
+ entries?: readonly { readonly name: string; readonly description: string }[];
18
27
  /** 条目 id:同 id 的最新一条取代更早的同 id 条目。本插件早先落库的消息没有它。 */
19
28
  id?: string;
20
29
  }
@@ -27,7 +36,7 @@ declare module "@deepseek-ai/dsh-llm" {
27
36
 
28
37
  /**
29
38
  * 这条消息是不是通道注入的条目——判据是 source 里的幂等键,而不是 kind:接管上游那两面的条目
30
- * 用上游 kind(`agent-instructions` / `skill-catalog`),幂等必须照样认。
39
+ * 接管工作区指令那一面的条目用上游 kind(`agent-instructions`),幂等必须照样认。
31
40
  */
32
41
  export function promptEntryIdOf(source: MessageSource): string | undefined {
33
42
  const id = (source as { readonly id?: unknown }).id;
@@ -79,7 +88,7 @@ export function latestReminderText(agent: Agent, id: string): string | undefined
79
88
 
80
89
  /**
81
90
  * 注入一条提醒:`text` 是已渲染好的完整正文(规则块或内容块),`key` 是它的幂等键。
82
- * `source` 声明这条消息对外的身份——接管上游那两面用上游 kind,我们自己的条目留默认。
91
+ * `source` 声明这条消息对外的身份——工作区指令那一面用上游 kind,我们自己的条目留默认。
83
92
  */
84
93
  export function reminderMessage(key: string, text: string, source?: MessageSource): UserMessage {
85
94
  return createUserMessage({
@@ -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
- // 对外身份沿用上游 kind:客户端标签与按 kind 认领的消费方认得这是技能目录,`entries` 就是
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: "skill-catalog",
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
- function registerSkillTool(ctx: Context): void {
96
- const skillTool = defineTool({
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
@@ -1,9 +0,0 @@
1
- # 本文件由 packages/context/dsh-context-assembler/tool/patch.ts 生成,请勿手工编辑
2
- - insert:
3
- - id: context-assembler
4
- name: "@morlay/dsh-context-assembler"
5
- config:
6
- capabilities:
7
- - assembler
8
- - agent-instructions
9
- - skill-catalog
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
- }