@morlay/dsh-context-assembler 0.0.1-alpha.2
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/LICENSE +21 -0
- package/README.md +154 -0
- package/cordis.patch.yml +9 -0
- package/dist/agent-instructions-BNEiZl0P.mjs +202 -0
- package/dist/agent-instructions.d.mts +27 -0
- package/dist/agent-instructions.mjs +2 -0
- package/dist/assembler-_F9urbPj.mjs +409 -0
- package/dist/assembler.d.mts +2 -0
- package/dist/assembler.mjs +2 -0
- package/dist/index-BGkeEaQA.d.mts +196 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.mjs +29 -0
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/rows.d.mts +46 -0
- package/dist/rows.mjs +50 -0
- package/dist/scope-CI-fNq5A.mjs +104 -0
- package/dist/scope.d.mts +47 -0
- package/dist/scope.mjs +2 -0
- package/dist/skill-catalog.d.mts +16 -0
- package/dist/skill-catalog.mjs +184 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +85 -0
- package/src/agent-instructions/baseline.ts +75 -0
- package/src/agent-instructions/files.ts +117 -0
- package/src/agent-instructions/index.ts +107 -0
- package/src/assembler/channel.ts +285 -0
- package/src/assembler/defaults.ts +53 -0
- package/src/assembler/index.ts +129 -0
- package/src/assembler/reminder.ts +92 -0
- package/src/assembler/rules.ts +14 -0
- package/src/index.ts +56 -0
- package/src/rows.ts +64 -0
- package/src/scope/index.ts +118 -0
- package/src/skill-catalog/index.ts +177 -0
- package/tool/patch.ts +43 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
4
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
|
|
6
|
+
//#region src/assembler/reminder.ts
|
|
7
|
+
const REMINDER_OPEN = "<system-reminder";
|
|
8
|
+
const REMINDER_CLOSE = "</system-reminder>";
|
|
9
|
+
/**
|
|
10
|
+
* 覆盖规则在系统提示词里声明一次(见 [`rules.ts`](./rules.ts)),所以每条 reminder 只带 id 与正文,
|
|
11
|
+
* 不再重复解释"最新一条覆盖更早的"。
|
|
12
|
+
*/
|
|
13
|
+
const RULES_SECTION = "assembler:rules";
|
|
14
|
+
/**
|
|
15
|
+
* 这条消息是不是通道注入的条目——判据是 source 里的幂等键,而不是 kind:接管上游那两面的条目
|
|
16
|
+
* 用上游 kind(`agent-instructions` / `skill-catalog`),幂等必须照样认。
|
|
17
|
+
*/
|
|
18
|
+
function promptEntryIdOf(source) {
|
|
19
|
+
const id = source.id;
|
|
20
|
+
return typeof id === "string" ? id : void 0;
|
|
21
|
+
}
|
|
22
|
+
function escapeFrameBody(body) {
|
|
23
|
+
return body.replaceAll(REMINDER_CLOSE, "<\\/system-reminder>");
|
|
24
|
+
}
|
|
25
|
+
function escapeAttr(value) {
|
|
26
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<");
|
|
27
|
+
}
|
|
28
|
+
function renderReminder(id, body) {
|
|
29
|
+
return `${REMINDER_OPEN} id="${escapeAttr(id)}">\n${escapeFrameBody(body)}\n${REMINDER_CLOSE}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 虚拟 skill(运行时注册、没有资源目录)的正文形态:只有 `<skill_instructions>`,
|
|
33
|
+
* 不渲染上游 `renderSkillContent` 的 `<skill_resources>`(那段对虚拟 skill 是噪音)。
|
|
34
|
+
*/
|
|
35
|
+
function renderVirtualSkill(name, content) {
|
|
36
|
+
return [
|
|
37
|
+
`<skill_content name="${escapeAttr(name)}">`,
|
|
38
|
+
"<skill_instructions>",
|
|
39
|
+
content,
|
|
40
|
+
"</skill_instructions>",
|
|
41
|
+
"</skill_content>"
|
|
42
|
+
].join("\n");
|
|
43
|
+
}
|
|
44
|
+
/** surface 上最近一条该 id 的 reminder 原文(含信封);没有则 undefined。 */
|
|
45
|
+
function latestReminderText(agent, id) {
|
|
46
|
+
for (const seq of agent.session.surface.nodes.toReversed()) {
|
|
47
|
+
const event = agent.session.eventAt(seq);
|
|
48
|
+
if (event?.type !== "user/message") continue;
|
|
49
|
+
if (promptEntryIdOf(event.data.source) !== id) continue;
|
|
50
|
+
const [block] = event.data.content;
|
|
51
|
+
return event.data.content.length === 1 && block?.type === "text" ? block.text : "";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 注入一条提醒:`text` 是已渲染好的完整正文(规则块或内容块),`key` 是它的幂等键。
|
|
56
|
+
* `source` 声明这条消息对外的身份——接管上游那两面用上游 kind,我们自己的条目留默认。
|
|
57
|
+
*/
|
|
58
|
+
function reminderMessage(key, text, source) {
|
|
59
|
+
return createUserMessage({
|
|
60
|
+
content: [{
|
|
61
|
+
type: "text",
|
|
62
|
+
text
|
|
63
|
+
}],
|
|
64
|
+
source: {
|
|
65
|
+
...source ?? {
|
|
66
|
+
kind: "context-assembler",
|
|
67
|
+
form: "instructions"
|
|
68
|
+
},
|
|
69
|
+
id: key
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/assembler/channel.ts
|
|
75
|
+
/**
|
|
76
|
+
* 注入通道:调用方声明「什么内容、怎么到达模型」,这里负责到达方式。
|
|
77
|
+
*
|
|
78
|
+
* - `on-demand`:注册成模型可用 skill(进 skill 目录,正文按需加载);
|
|
79
|
+
* - `auto`:正文自动注入 reminder(注册成 skill 但标 `modelInvocable: false`,因为已经常驻);
|
|
80
|
+
* - `replaceSection` / `suppressSection`:装配结果上的文本改写与丢弃。
|
|
81
|
+
*
|
|
82
|
+
* 覆盖语义(同 id 最新取代更早)由系统提示词里的声明说明一次,见 [`rules.ts`](./rules.ts)。
|
|
83
|
+
*/
|
|
84
|
+
var ContextAssembler = class extends Service {
|
|
85
|
+
host;
|
|
86
|
+
declarations = /* @__PURE__ */ new Map();
|
|
87
|
+
rules = /* @__PURE__ */ new Map();
|
|
88
|
+
overrides = /* @__PURE__ */ new Map();
|
|
89
|
+
suppressed = /* @__PURE__ */ new Set();
|
|
90
|
+
skills = /* @__PURE__ */ new Map();
|
|
91
|
+
states = /* @__PURE__ */ new WeakMap();
|
|
92
|
+
/** 不要 instruction 类注入(规则块)的会话;内容块(技能正文、引用材料)不受它管。 */
|
|
93
|
+
withoutInstructions = /* @__PURE__ */ new WeakSet();
|
|
94
|
+
/** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
|
|
95
|
+
toolScopes = /* @__PURE__ */ new WeakMap();
|
|
96
|
+
/** `host` 是构造期那个根 ctx:`skills` 的访问权限挂在插件的 inject 声明上,不能依赖访问者 ctx。 */
|
|
97
|
+
constructor(host) {
|
|
98
|
+
super(host, "contextAssembler");
|
|
99
|
+
this.host = host;
|
|
100
|
+
}
|
|
101
|
+
registerSkill(declaration) {
|
|
102
|
+
this.declarations.set(declaration.name, declaration);
|
|
103
|
+
this.refreshSkills();
|
|
104
|
+
return () => {
|
|
105
|
+
if (this.declarations.get(declaration.name) !== declaration) return;
|
|
106
|
+
this.declarations.delete(declaration.name);
|
|
107
|
+
this.refreshSkills();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
registerRule(declaration) {
|
|
111
|
+
this.rules.set(declaration.id, declaration);
|
|
112
|
+
return () => {
|
|
113
|
+
if (this.rules.get(declaration.id) === declaration) this.rules.delete(declaration.id);
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
replaceSection(name, text) {
|
|
117
|
+
this.overrides.set(name, text);
|
|
118
|
+
return () => {
|
|
119
|
+
if (this.overrides.get(name) === text) this.overrides.delete(name);
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
suppressSection(name) {
|
|
123
|
+
this.suppressed.add(name);
|
|
124
|
+
return () => {
|
|
125
|
+
this.suppressed.delete(name);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
isSuppressed(name) {
|
|
129
|
+
return this.suppressed.has(name);
|
|
130
|
+
}
|
|
131
|
+
/** 装配结果上该 section 要换成的文本;undefined 表示原样保留。 */
|
|
132
|
+
replacement(name, agent) {
|
|
133
|
+
const override = this.overrides.get(name);
|
|
134
|
+
return override === void 0 || agent === void 0 ? void 0 : override(agent);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
|
|
138
|
+
* `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
|
|
139
|
+
*/
|
|
140
|
+
/** 这个会话不要 instruction 类的规则块(模式说了 `instructions: false`)。 */
|
|
141
|
+
setInstructions(agent, on) {
|
|
142
|
+
if (on) this.withoutInstructions.delete(agent);
|
|
143
|
+
else this.withoutInstructions.add(agent);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* 本会话的**工具收窄**(模式白名单):投影层只过滤装配结果、不碰工具注册表,所以"谁能用"这件事
|
|
147
|
+
* 必须在通道上登记,技能目录与组正文才收得住。与 {@link setInstructions} 同形——谓词由模式那一行给
|
|
148
|
+
* (`context-scope`),通道自己不碰 `tools` 服务。
|
|
149
|
+
*/
|
|
150
|
+
restrictTools(agent, allowed) {
|
|
151
|
+
this.toolScopes.set(agent, allowed);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 本会话的工具可见性:调用方给出的注册表可见性 × 已登记的模式收窄。技能目录与组正文共用它,
|
|
155
|
+
* 免得同一套判据在几处各写一遍。
|
|
156
|
+
*/
|
|
157
|
+
visibleTools(agent, registered) {
|
|
158
|
+
const allowed = this.toolScopes.get(agent);
|
|
159
|
+
return allowed === void 0 ? registered : (tool) => registered(tool) && allowed(tool);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* 该会话看不到的 skill 名:声明了 `requires` 而依赖的工具一个都不可见。
|
|
163
|
+
*
|
|
164
|
+
* 目录由 `context-skill-catalog` 渲染,它把工具可见性作为谓词传进来(通道不碰 tools 服务)。
|
|
165
|
+
*/
|
|
166
|
+
hiddenSkills(visible) {
|
|
167
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
168
|
+
for (const [name, declaration] of this.declarations) {
|
|
169
|
+
const requires = declaration.requires;
|
|
170
|
+
if (requires === void 0 || requires.length === 0) continue;
|
|
171
|
+
if (!requires.some(visible)) hidden.add(name);
|
|
172
|
+
}
|
|
173
|
+
return hidden;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* 该 skill 按会话修剪后的正文:注册表只能存一份(技能注册是装配期一次、全局可见),所以按需加载
|
|
177
|
+
* 路径(接管的 `skill` 工具)要用这里重新算一次,才和 `auto` 正文一样跟着工具走。
|
|
178
|
+
*/
|
|
179
|
+
contentFor(name, agent) {
|
|
180
|
+
return this.declarations.get(name)?.content(agent);
|
|
181
|
+
}
|
|
182
|
+
async collect(agent) {
|
|
183
|
+
const entries = /* @__PURE__ */ new Map();
|
|
184
|
+
for (const declaration of this.declarations.values()) {
|
|
185
|
+
if ((declaration.injection ?? "on-demand") !== "auto") continue;
|
|
186
|
+
const body = declaration.content(agent);
|
|
187
|
+
if (body.length > 0) entries.set(declaration.name, { text: renderVirtualSkill(declaration.name, body) });
|
|
188
|
+
}
|
|
189
|
+
const instructionsOff = this.withoutInstructions.has(agent);
|
|
190
|
+
for (const declaration of this.rules.values()) {
|
|
191
|
+
if (instructionsOff) break;
|
|
192
|
+
const text = await withTimeout(declaration.text(agent), CONTENT_TIMEOUT_MS);
|
|
193
|
+
if (text.length > 0) entries.set(declaration.id, {
|
|
194
|
+
text: renderReminder(declaration.id, text),
|
|
195
|
+
...declaration.source === void 0 ? {} : { source: declaration.source(agent) }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (!instructionsOff) for (const [id, text] of this.sectionsOf(agent)) entries.set(id, { text: renderReminder(id, text) });
|
|
199
|
+
return entries;
|
|
200
|
+
}
|
|
201
|
+
sectionsOf(agent) {
|
|
202
|
+
return this.states.get(agent)?.sections ?? /* @__PURE__ */ new Map();
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 每步装配调一次:记下本步降级出来的 section 文本,供下一步注入。
|
|
206
|
+
*/
|
|
207
|
+
sync(agent, input) {
|
|
208
|
+
this.stateOf(agent).sections = input.sections;
|
|
209
|
+
}
|
|
210
|
+
/** skill 注册(装配期一次,全局可见):正文没变的不重注册。 */
|
|
211
|
+
refreshSkills(agent) {
|
|
212
|
+
for (const [name, declaration] of this.declarations) {
|
|
213
|
+
const content = declaration.content(agent);
|
|
214
|
+
const modelInvocable = (declaration.injection ?? "on-demand") === "on-demand";
|
|
215
|
+
const digest = `${modelInvocable ? "model" : "auto"}\u0000${declaration.description}\u0000${content}`;
|
|
216
|
+
const current = this.skills.get(name);
|
|
217
|
+
if (current?.digest === digest) continue;
|
|
218
|
+
current?.dispose();
|
|
219
|
+
const dispose = this.host.skills.register({
|
|
220
|
+
name,
|
|
221
|
+
description: declaration.description,
|
|
222
|
+
content,
|
|
223
|
+
source: "runtime",
|
|
224
|
+
invocation: {
|
|
225
|
+
modelInvocable,
|
|
226
|
+
userInvocable: true
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
this.skills.set(name, {
|
|
230
|
+
digest,
|
|
231
|
+
dispose
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
for (const [name, registered] of this.skills) {
|
|
235
|
+
if (this.declarations.has(name)) continue;
|
|
236
|
+
registered.dispose();
|
|
237
|
+
this.skills.delete(name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
stateOf(agent) {
|
|
241
|
+
const existing = this.states.get(agent);
|
|
242
|
+
if (existing !== void 0) return existing;
|
|
243
|
+
const state = { sections: /* @__PURE__ */ new Map() };
|
|
244
|
+
this.states.set(agent, state);
|
|
245
|
+
return state;
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
/** 一条注入内容的取用上限:超过就按"没有内容"处理,不阻塞这一轮请求。 */
|
|
249
|
+
const CONTENT_TIMEOUT_MS = 2e3;
|
|
250
|
+
async function withTimeout(work, ms) {
|
|
251
|
+
if (typeof work === "string") return work;
|
|
252
|
+
const settled = Promise.resolve(work).catch(() => "");
|
|
253
|
+
let timer;
|
|
254
|
+
const expiry = new Promise((resolve) => {
|
|
255
|
+
timer = setTimeout(() => {
|
|
256
|
+
resolve("");
|
|
257
|
+
}, ms);
|
|
258
|
+
});
|
|
259
|
+
try {
|
|
260
|
+
return await Promise.race([settled, expiry]);
|
|
261
|
+
} finally {
|
|
262
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/assembler/defaults.ts
|
|
267
|
+
/**
|
|
268
|
+
* 留在系统提示词里的 section:部署级 persona(子 agent 在 agent 作用域注册的同名 persona 也命中)
|
|
269
|
+
* 与覆盖规则声明([`rules.ts`](./rules.ts))。
|
|
270
|
+
*/
|
|
271
|
+
const DEFAULT_KEEP = [
|
|
272
|
+
PERSONA_PREFIX_SECTION,
|
|
273
|
+
PERSONA_SUFFIX_SECTION,
|
|
274
|
+
RULES_SECTION
|
|
275
|
+
];
|
|
276
|
+
/**
|
|
277
|
+
* 不进提示词的 section。三类:
|
|
278
|
+
*
|
|
279
|
+
* 1. **harness 自带的开场白与平台运维说明**:`harness:identity`("You are an AI agent powered by
|
|
280
|
+
* DeepSeek Harness."——身份由各模式的 persona 给,这句话在本部署里是噪音,且是英文)、
|
|
281
|
+
* `harness:source`(讲 dev server / HMR / 本地 checkout 位置);
|
|
282
|
+
* 2. 本部署不装配的工具的用法说明:`tool:lsp` / `tool:pty` / `tool:session-query` / `tool:cordis` /
|
|
283
|
+
* `tool:ralph`;
|
|
284
|
+
* 3. `app:web-surface`(平台外壳说明)。
|
|
285
|
+
*
|
|
286
|
+
* 其余工具说明由各组的 `drops` 摘掉、要点由 toolkit 的 `guidance` 出口写进组正文,不在这一份里。
|
|
287
|
+
*/
|
|
288
|
+
const DEFAULT_SUPPRESS = [
|
|
289
|
+
"harness:identity",
|
|
290
|
+
"harness:source",
|
|
291
|
+
"app:web-surface",
|
|
292
|
+
"tool:lsp",
|
|
293
|
+
"tool:pty",
|
|
294
|
+
"tool:session-query",
|
|
295
|
+
"tool:cordis",
|
|
296
|
+
"tool:ralph"
|
|
297
|
+
];
|
|
298
|
+
/**
|
|
299
|
+
* 改写成中文的两段上游说明(内容忠实原意):`@` 引用路径的语义、输出里的文件链接规范。
|
|
300
|
+
*
|
|
301
|
+
* 这两段注册在 agent 层(`context:file-reference`)与 host 层(`ui:deliverable-file-references`),
|
|
302
|
+
* 注册层不归我们——所以在装配结果上换文本(不移除:内容要保留)。
|
|
303
|
+
*/
|
|
304
|
+
const DEFAULT_REPLACE = {
|
|
305
|
+
"context:file-reference": "以 @ 开头的词是用户显式引用的工作区路径,相对于工作区根。结尾带斜杠表示目录:需要时列目录内容。其余是文件:需要内容时用 read 工具读取,不要在读取前声称自己已经看过。用 @\"...\" 括住含空格的路径。",
|
|
306
|
+
"ui:deliverable-file-references": "成功创建或修改文件后,在最终回复里点明主要产物。除命令、配置表达式与代码块之外,每处提到已存在的文件都要链到它的完整路径(相对工作目录或绝对路径);已知行号时在目标后加 `#L24` 或 `#L24-L30`。链接文本用文件名或清晰别名,只加够区分的父目录,不要把完整路径放进标签;默认只用文件名,需要精确位置时在标签后接 `:24` 或 `:24–30`(不带 `#` 或 `L`)。"
|
|
307
|
+
};
|
|
308
|
+
//#endregion
|
|
309
|
+
//#region src/assembler/rules.ts
|
|
310
|
+
/**
|
|
311
|
+
* 覆盖规则的声明文本:写进**系统提示词**(不在 reminder 里重复),所以每条 reminder 只带 id 与正文。
|
|
312
|
+
*
|
|
313
|
+
* 为什么放系统提示词而不是首条 reminder:它是固定文本(不随会话变,不付缓存代价),而且放在这里
|
|
314
|
+
* 才是系统级规则该在的位置——首条 reminder 自身会被压缩、被覆盖,权威性更低。
|
|
315
|
+
*/
|
|
316
|
+
const RULES_TEXT = ["带 id 的 system-reminder 块是本系统提示词的一部分,持续有效:同 id 只有最新一条生效并取代更早的,不同 id 互不影响。", "skill_content 是要遵循的技能说明(随提示送来的与你后来加载的一样),file_content 是本步引用的文件内容。"].join("");
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/assembler/index.ts
|
|
319
|
+
var assembler_exports = /* @__PURE__ */ __exportAll({
|
|
320
|
+
Config: () => Config,
|
|
321
|
+
ContextAssembler: () => ContextAssembler,
|
|
322
|
+
DEFAULT_KEEP: () => DEFAULT_KEEP,
|
|
323
|
+
DEFAULT_REPLACE: () => DEFAULT_REPLACE,
|
|
324
|
+
DEFAULT_SUPPRESS: () => DEFAULT_SUPPRESS,
|
|
325
|
+
apply: () => apply,
|
|
326
|
+
inject: () => inject,
|
|
327
|
+
name: () => name,
|
|
328
|
+
renderVirtualSkill: () => renderVirtualSkill
|
|
329
|
+
});
|
|
330
|
+
const name = "context-assembler";
|
|
331
|
+
/** 规则声明的位置:紧跟部署 persona(order 0),在任何降级内容之前。 */
|
|
332
|
+
const RULES_ORDER = 1;
|
|
333
|
+
const inject = ["systemPrompt", "skills"];
|
|
334
|
+
const Config = z.object({
|
|
335
|
+
keep: z.array(z.string()).default([...DEFAULT_KEEP]),
|
|
336
|
+
suppress: z.array(z.string()).default([...DEFAULT_SUPPRESS]),
|
|
337
|
+
replace: z.dict(z.string()).default({ ...DEFAULT_REPLACE })
|
|
338
|
+
});
|
|
339
|
+
/**
|
|
340
|
+
* 提示词注入的唯一通道:system prompt 里只留 `keep`,其余内容按声明的方式到达模型——
|
|
341
|
+
* 降级为紧随用户消息的 reminder、或直接丢弃(写进按需 skill 正文是调用方自己的事)。
|
|
342
|
+
*/
|
|
343
|
+
function apply(ctx, config) {
|
|
344
|
+
const channel = new ContextAssembler(ctx);
|
|
345
|
+
const keep = new Set(config.keep ?? DEFAULT_KEEP);
|
|
346
|
+
for (const name of config.suppress ?? DEFAULT_SUPPRESS) channel.suppressSection(name);
|
|
347
|
+
for (const [name, text] of Object.entries(config.replace ?? DEFAULT_REPLACE)) channel.replaceSection(name, () => text);
|
|
348
|
+
ctx.systemPrompt.section({
|
|
349
|
+
name: RULES_SECTION,
|
|
350
|
+
order: RULES_ORDER,
|
|
351
|
+
text: RULES_TEXT
|
|
352
|
+
});
|
|
353
|
+
ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
|
|
354
|
+
const result = await next();
|
|
355
|
+
const agent = context.agent;
|
|
356
|
+
const demoted = /* @__PURE__ */ new Map();
|
|
357
|
+
const sections = [];
|
|
358
|
+
let changed = false;
|
|
359
|
+
for (const section of result.sections) {
|
|
360
|
+
if (channel.isSuppressed(section.name)) {
|
|
361
|
+
changed = true;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const text = channel.replacement(section.name, agent) ?? section.text;
|
|
365
|
+
if (text !== section.text) changed = true;
|
|
366
|
+
if (keep.has(section.name) || text.length === 0) {
|
|
367
|
+
sections.push(text === section.text ? section : {
|
|
368
|
+
...section,
|
|
369
|
+
text
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
changed = true;
|
|
374
|
+
demoted.set(sectionId(section.name), text);
|
|
375
|
+
}
|
|
376
|
+
if (agent !== void 0) channel.sync(agent, { sections: demoted });
|
|
377
|
+
return changed ? {
|
|
378
|
+
...result,
|
|
379
|
+
sections
|
|
380
|
+
} : result;
|
|
381
|
+
});
|
|
382
|
+
ctx.on("agent/pre-step", async ({ agent, messages }, next) => {
|
|
383
|
+
const decision = await next();
|
|
384
|
+
if (decision.kind === "reject" || decision.messages.length === 0) return decision;
|
|
385
|
+
const pending = await pendingEntries(agent, channel);
|
|
386
|
+
if (pending.length === 0) return decision;
|
|
387
|
+
const claimedEnd = decision.messages.findLastIndex((message) => messages.includes(message));
|
|
388
|
+
return {
|
|
389
|
+
...decision,
|
|
390
|
+
messages: decision.messages.toSpliced(claimedEnd + 1, 0, ...pending.map(([key, entry]) => reminderMessage(key, entry.text, entry.source)))
|
|
391
|
+
};
|
|
392
|
+
});
|
|
393
|
+
ctx.on("agent/request-error", async ({ agent, signal }, next) => {
|
|
394
|
+
const action = await next();
|
|
395
|
+
if (action?.kind !== "retry" || signal.aborted) return action;
|
|
396
|
+
for (const [key, entry] of await pendingEntries(agent, channel)) agent.session.append("user/message", reminderMessage(key, entry.text, entry.source), { surfaceOp: "append" });
|
|
397
|
+
return action;
|
|
398
|
+
}, { prepend: true });
|
|
399
|
+
}
|
|
400
|
+
/** 降级 section 的条目 id:一条 section 一个 id,于是同一次变化只重发那一条。 */
|
|
401
|
+
function sectionId(name) {
|
|
402
|
+
return `section:${name}`;
|
|
403
|
+
}
|
|
404
|
+
/** 本步要注入的条目:键相同且文本未变就不注入,按键字典序(顺序不随注册顺序抖动)。 */
|
|
405
|
+
async function pendingEntries(agent, channel) {
|
|
406
|
+
return [...await channel.collect(agent)].filter(([key, entry]) => latestReminderText(agent, key) !== entry.text).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
407
|
+
}
|
|
408
|
+
//#endregion
|
|
409
|
+
export { name as a, DEFAULT_SUPPRESS as c, inject as i, ContextAssembler as l, apply as n, DEFAULT_KEEP as o, assembler_exports as r, DEFAULT_REPLACE as s, Config as t, renderVirtualSkill as u };
|
|
@@ -0,0 +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";
|
|
2
|
+
export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, type InjectionMode, type PromptEntry, type PromptRuleDeclaration, type PromptSkillDeclaration, apply, inject, name, renderVirtualSkill };
|
|
@@ -0,0 +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";
|
|
2
|
+
export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, apply, inject, name, renderVirtualSkill };
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Context, Service } from "@deepseek-ai/cordis";
|
|
3
|
+
import { MessageSource } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { Agent } from "@deepseek-ai/dsh-agent";
|
|
5
|
+
import "@deepseek-ai/dsh-session";
|
|
6
|
+
//#region src/assembler/defaults.d.ts
|
|
7
|
+
/**
|
|
8
|
+
* 留在系统提示词里的 section:部署级 persona(子 agent 在 agent 作用域注册的同名 persona 也命中)
|
|
9
|
+
* 与覆盖规则声明([`rules.ts`](./rules.ts))。
|
|
10
|
+
*/
|
|
11
|
+
declare const DEFAULT_KEEP: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* 不进提示词的 section。三类:
|
|
14
|
+
*
|
|
15
|
+
* 1. **harness 自带的开场白与平台运维说明**:`harness:identity`("You are an AI agent powered by
|
|
16
|
+
* DeepSeek Harness."——身份由各模式的 persona 给,这句话在本部署里是噪音,且是英文)、
|
|
17
|
+
* `harness:source`(讲 dev server / HMR / 本地 checkout 位置);
|
|
18
|
+
* 2. 本部署不装配的工具的用法说明:`tool:lsp` / `tool:pty` / `tool:session-query` / `tool:cordis` /
|
|
19
|
+
* `tool:ralph`;
|
|
20
|
+
* 3. `app:web-surface`(平台外壳说明)。
|
|
21
|
+
*
|
|
22
|
+
* 其余工具说明由各组的 `drops` 摘掉、要点由 toolkit 的 `guidance` 出口写进组正文,不在这一份里。
|
|
23
|
+
*/
|
|
24
|
+
declare const DEFAULT_SUPPRESS: readonly string[];
|
|
25
|
+
/**
|
|
26
|
+
* 改写成中文的两段上游说明(内容忠实原意):`@` 引用路径的语义、输出里的文件链接规范。
|
|
27
|
+
*
|
|
28
|
+
* 这两段注册在 agent 层(`context:file-reference`)与 host 层(`ui:deliverable-file-references`),
|
|
29
|
+
* 注册层不归我们——所以在装配结果上换文本(不移除:内容要保留)。
|
|
30
|
+
*/
|
|
31
|
+
declare const DEFAULT_REPLACE: Readonly<Record<string, string>>;
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/assembler/channel.d.ts
|
|
34
|
+
/** 正文到达模型的方式:自动注入,或等模型按需加载。 */
|
|
35
|
+
type InjectionMode = "auto" | "on-demand";
|
|
36
|
+
/**
|
|
37
|
+
* 一条按 skill 形态声明的注入:正文是本仓库维护的文案。
|
|
38
|
+
*
|
|
39
|
+
* 正文与 agent 无关(工具集在装配期就定了),所以 skill 在装配期注册一次、全局可见。
|
|
40
|
+
*/
|
|
41
|
+
interface PromptSkillDeclaration {
|
|
42
|
+
/** kebab-case:skill 名,也是注入条目的 id。 */
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly title: string;
|
|
45
|
+
/** 目录行摘要:讲清什么时候该加载它。 */
|
|
46
|
+
readonly description: string;
|
|
47
|
+
/**
|
|
48
|
+
* 正文按**会话**生成:模式可能只给一部分工具,正文要跟着修剪(不该讲用不到的工具)。
|
|
49
|
+
* 不传 agent(装配期注册 skill 时)返回不过滤的完整版。
|
|
50
|
+
*/
|
|
51
|
+
readonly content: (agent?: Agent) => string;
|
|
52
|
+
/**
|
|
53
|
+
* 这个 skill 赖以成立的**入口工具**(任一存在即可,不是全部)。都不可见时它不该出现在该会话的技能
|
|
54
|
+
* 目录里——"不能用 skill 工具就没有技能目录"是同一个道理:技能也跟着工具走。
|
|
55
|
+
*
|
|
56
|
+
* 缺省语义就是"一个都不在就藏起来";声明方要自己挑入口,别把组内所有可能的工具都倒进来
|
|
57
|
+
* (子代理控制行与团队插件会提供同名工具,全集当判据会把不该出现的 skill 留下)。
|
|
58
|
+
*/
|
|
59
|
+
readonly requires?: readonly string[];
|
|
60
|
+
/** 缺省 `on-demand`:只有正文确有必要常驻时才写 `auto`。 */
|
|
61
|
+
readonly injection?: InjectionMode;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 一条规则块声明:系统级信息(工作区指令、skill 目录),文本按 id 幂等注入、同 id 覆盖。
|
|
65
|
+
* 文本可以是异步的(skill 目录要读 skill 注册表)。
|
|
66
|
+
*/
|
|
67
|
+
interface PromptRuleDeclaration {
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly text: (agent: Agent) => string | Promise<string>;
|
|
70
|
+
/**
|
|
71
|
+
* 这条规则的对外身份。接管上游那两面(工作区指令 / skill 目录)按上游 kind 发消息,好让按 kind 认领的
|
|
72
|
+
* 消费方(客户端标签、上游的实验性约束收集)认得出来;不声明就是通道自己的 `context-assembler`。
|
|
73
|
+
* 幂等键仍是 `id`(`reminderMessage` 会把它塞进 source)。
|
|
74
|
+
*/
|
|
75
|
+
readonly source?: (agent: Agent) => MessageSource;
|
|
76
|
+
}
|
|
77
|
+
/** 一条待注入的条目:正文已渲染好,`source` 是它声明给外部的身份。 */
|
|
78
|
+
interface PromptEntry {
|
|
79
|
+
readonly text: string;
|
|
80
|
+
readonly source?: MessageSource;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 注入通道:调用方声明「什么内容、怎么到达模型」,这里负责到达方式。
|
|
84
|
+
*
|
|
85
|
+
* - `on-demand`:注册成模型可用 skill(进 skill 目录,正文按需加载);
|
|
86
|
+
* - `auto`:正文自动注入 reminder(注册成 skill 但标 `modelInvocable: false`,因为已经常驻);
|
|
87
|
+
* - `replaceSection` / `suppressSection`:装配结果上的文本改写与丢弃。
|
|
88
|
+
*
|
|
89
|
+
* 覆盖语义(同 id 最新取代更早)由系统提示词里的声明说明一次,见 [`rules.ts`](./rules.ts)。
|
|
90
|
+
*/
|
|
91
|
+
declare class ContextAssembler extends Service {
|
|
92
|
+
private readonly host;
|
|
93
|
+
private readonly declarations;
|
|
94
|
+
private readonly rules;
|
|
95
|
+
private readonly overrides;
|
|
96
|
+
private readonly suppressed;
|
|
97
|
+
private readonly skills;
|
|
98
|
+
private readonly states;
|
|
99
|
+
/** 不要 instruction 类注入(规则块)的会话;内容块(技能正文、引用材料)不受它管。 */
|
|
100
|
+
private readonly withoutInstructions;
|
|
101
|
+
/** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
|
|
102
|
+
private readonly toolScopes;
|
|
103
|
+
/** `host` 是构造期那个根 ctx:`skills` 的访问权限挂在插件的 inject 声明上,不能依赖访问者 ctx。 */
|
|
104
|
+
constructor(host: Context);
|
|
105
|
+
registerSkill(declaration: PromptSkillDeclaration): () => void;
|
|
106
|
+
registerRule(declaration: PromptRuleDeclaration): () => void;
|
|
107
|
+
replaceSection(name: string, text: (agent: Agent) => string | undefined): () => void;
|
|
108
|
+
suppressSection(name: string): () => void;
|
|
109
|
+
isSuppressed(name: string): boolean;
|
|
110
|
+
/** 装配结果上该 section 要换成的文本;undefined 表示原样保留。 */
|
|
111
|
+
replacement(name: string, agent: Agent | undefined): string | undefined;
|
|
112
|
+
/**
|
|
113
|
+
* 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
|
|
114
|
+
* `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
|
|
115
|
+
*/
|
|
116
|
+
/** 这个会话不要 instruction 类的规则块(模式说了 `instructions: false`)。 */
|
|
117
|
+
setInstructions(agent: Agent, on: boolean): void;
|
|
118
|
+
/**
|
|
119
|
+
* 本会话的**工具收窄**(模式白名单):投影层只过滤装配结果、不碰工具注册表,所以"谁能用"这件事
|
|
120
|
+
* 必须在通道上登记,技能目录与组正文才收得住。与 {@link setInstructions} 同形——谓词由模式那一行给
|
|
121
|
+
* (`context-scope`),通道自己不碰 `tools` 服务。
|
|
122
|
+
*/
|
|
123
|
+
restrictTools(agent: Agent, allowed: (tool: string) => boolean): void;
|
|
124
|
+
/**
|
|
125
|
+
* 本会话的工具可见性:调用方给出的注册表可见性 × 已登记的模式收窄。技能目录与组正文共用它,
|
|
126
|
+
* 免得同一套判据在几处各写一遍。
|
|
127
|
+
*/
|
|
128
|
+
visibleTools(agent: Agent, registered: (tool: string) => boolean): (tool: string) => boolean;
|
|
129
|
+
/**
|
|
130
|
+
* 该会话看不到的 skill 名:声明了 `requires` 而依赖的工具一个都不可见。
|
|
131
|
+
*
|
|
132
|
+
* 目录由 `context-skill-catalog` 渲染,它把工具可见性作为谓词传进来(通道不碰 tools 服务)。
|
|
133
|
+
*/
|
|
134
|
+
hiddenSkills(visible: (tool: string) => boolean): ReadonlySet<string>;
|
|
135
|
+
/**
|
|
136
|
+
* 该 skill 按会话修剪后的正文:注册表只能存一份(技能注册是装配期一次、全局可见),所以按需加载
|
|
137
|
+
* 路径(接管的 `skill` 工具)要用这里重新算一次,才和 `auto` 正文一样跟着工具走。
|
|
138
|
+
*/
|
|
139
|
+
contentFor(name: string, agent: Agent | undefined): string | undefined;
|
|
140
|
+
collect(agent: Agent): Promise<Map<string, PromptEntry>>;
|
|
141
|
+
private sectionsOf;
|
|
142
|
+
/**
|
|
143
|
+
* 每步装配调一次:记下本步降级出来的 section 文本,供下一步注入。
|
|
144
|
+
*/
|
|
145
|
+
sync(agent: Agent, input: {
|
|
146
|
+
readonly sections: ReadonlyMap<string, string>;
|
|
147
|
+
}): void;
|
|
148
|
+
/** skill 注册(装配期一次,全局可见):正文没变的不重注册。 */
|
|
149
|
+
private refreshSkills;
|
|
150
|
+
private stateOf;
|
|
151
|
+
}
|
|
152
|
+
declare module "@deepseek-ai/cordis" {
|
|
153
|
+
interface Context {
|
|
154
|
+
contextAssembler: ContextAssembler;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
//#region src/assembler/reminder.d.ts
|
|
159
|
+
/** 通道自己的条目形态(注入方没声明 source 时的默认):规则块,幂等键是 id。 */
|
|
160
|
+
interface PromptReminderSource {
|
|
161
|
+
kind: "context-assembler";
|
|
162
|
+
form: "instructions";
|
|
163
|
+
/** 条目 id:同 id 的最新一条取代更早的同 id 条目。本插件早先落库的消息没有它。 */
|
|
164
|
+
id?: string;
|
|
165
|
+
}
|
|
166
|
+
declare module "@deepseek-ai/dsh-llm" {
|
|
167
|
+
interface MessageSourceMap {
|
|
168
|
+
"context-assembler": PromptReminderSource;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* 虚拟 skill(运行时注册、没有资源目录)的正文形态:只有 `<skill_instructions>`,
|
|
173
|
+
* 不渲染上游 `renderSkillContent` 的 `<skill_resources>`(那段对虚拟 skill 是噪音)。
|
|
174
|
+
*/
|
|
175
|
+
declare function renderVirtualSkill(name: string, content: string): string;
|
|
176
|
+
declare namespace index_d_exports {
|
|
177
|
+
export { Config, ContextAssembler, DEFAULT_KEEP, DEFAULT_REPLACE, DEFAULT_SUPPRESS, InjectionMode, PromptEntry, PromptRuleDeclaration, PromptSkillDeclaration, apply, inject, name, renderVirtualSkill };
|
|
178
|
+
}
|
|
179
|
+
declare const name = "context-assembler";
|
|
180
|
+
declare const inject: string[];
|
|
181
|
+
interface Config {
|
|
182
|
+
/** 留在系统提示词里的 section 名;其余非空 section 降级为 reminder。 */
|
|
183
|
+
keep?: string[];
|
|
184
|
+
/** 不进提示词的 section 名(部署级噪音)。 */
|
|
185
|
+
suppress?: string[];
|
|
186
|
+
/** 装配结果上改写的 section 文本(section 名 → 中文文案)。 */
|
|
187
|
+
replace?: Record<string, string>;
|
|
188
|
+
}
|
|
189
|
+
declare const Config: z<Config>;
|
|
190
|
+
/**
|
|
191
|
+
* 提示词注入的唯一通道:system prompt 里只留 `keep`,其余内容按声明的方式到达模型——
|
|
192
|
+
* 降级为紧随用户消息的 reminder、或直接丢弃(写进按需 skill 正文是调用方自己的事)。
|
|
193
|
+
*/
|
|
194
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
195
|
+
//#endregion
|
|
196
|
+
export { name as a, InjectionMode as c, PromptSkillDeclaration as d, DEFAULT_KEEP as f, inject as i, PromptEntry as l, DEFAULT_SUPPRESS as m, apply as n, renderVirtualSkill as o, DEFAULT_REPLACE as p, index_d_exports as r, ContextAssembler as s, Config as t, PromptRuleDeclaration as u };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { t as index_d_exports } from "./agent-instructions.mjs";
|
|
2
|
+
import { r as index_d_exports$1 } from "./index-BGkeEaQA.mjs";
|
|
3
|
+
import { t as index_d_exports$2 } from "./scope.mjs";
|
|
4
|
+
import { t as index_d_exports$3 } from "./skill-catalog.mjs";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
//#region src/index.d.ts
|
|
8
|
+
declare const name = "context-assembler-tree";
|
|
9
|
+
declare const CAPABILITIES: {
|
|
10
|
+
readonly assembler: typeof index_d_exports$1;
|
|
11
|
+
readonly "agent-instructions": typeof index_d_exports;
|
|
12
|
+
readonly "skill-catalog": typeof index_d_exports$3;
|
|
13
|
+
readonly scope: typeof index_d_exports$2;
|
|
14
|
+
};
|
|
15
|
+
type Capability = keyof typeof CAPABILITIES;
|
|
16
|
+
interface Config {
|
|
17
|
+
/** 装哪些能力(名字即子出口名);缺省全部——标准模式要完整的一套。未知名字在装载时报错。 */
|
|
18
|
+
capabilities?: string[];
|
|
19
|
+
/** 各能力自己的 config,按能力名给(例如 `{ scope: { allowTools: [...] } }`)。 */
|
|
20
|
+
options?: Record<string, Record<string, unknown>>;
|
|
21
|
+
}
|
|
22
|
+
declare const Config: z<Config>;
|
|
23
|
+
declare function apply(ctx: Context, config: Config): void;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { Capability, Config, apply, name };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { n as agent_instructions_exports } from "./agent-instructions-BNEiZl0P.mjs";
|
|
2
|
+
import { r as assembler_exports } from "./assembler-_F9urbPj.mjs";
|
|
3
|
+
import { a as scope_exports } from "./scope-CI-fNq5A.mjs";
|
|
4
|
+
import { t as skill_catalog_exports } from "./skill-catalog.mjs";
|
|
5
|
+
import z from "@deepseek-ai/schemastery";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
const name = "context-assembler-tree";
|
|
8
|
+
const CAPABILITIES = {
|
|
9
|
+
assembler: assembler_exports,
|
|
10
|
+
"agent-instructions": agent_instructions_exports,
|
|
11
|
+
"skill-catalog": skill_catalog_exports,
|
|
12
|
+
scope: scope_exports
|
|
13
|
+
};
|
|
14
|
+
const CAPABILITY_NAMES = Object.keys(CAPABILITIES);
|
|
15
|
+
const Config = z.object({
|
|
16
|
+
capabilities: z.array(z.string()).default([...CAPABILITY_NAMES]),
|
|
17
|
+
options: z.any().default({})
|
|
18
|
+
});
|
|
19
|
+
function apply(ctx, config) {
|
|
20
|
+
const wanted = config.capabilities ?? CAPABILITY_NAMES;
|
|
21
|
+
const options = config.options ?? {};
|
|
22
|
+
for (const name of wanted) {
|
|
23
|
+
const capability = CAPABILITIES[name];
|
|
24
|
+
if (capability === void 0) throw new Error(`context: unknown capability "${name}"(可选:${CAPABILITY_NAMES.join(" / ")})`);
|
|
25
|
+
ctx.plugin(capability, options[name] ?? {});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { Config, apply, name };
|