@morlay/dsh-context-assembler 0.0.1 → 0.0.3

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.
@@ -1,237 +0,0 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
- import z from "@deepseek-ai/schemastery";
3
- import { createHash } from "node:crypto";
4
- import { dirname, join, relative, resolve } from "node:path";
5
- import { readFile, stat } from "node:fs/promises";
6
- import { homedir } from "node:os";
7
- import { Config } from "@deepseek-ai/dsh-agent-instructions";
8
- //#region src/agent-instructions/files.ts
9
- /** 从 cwd 逐级向上找项目根(含标记目录的那一级)。 */
10
- async function projectRoot(cwd, marker = ".git") {
11
- let current = resolve(cwd);
12
- for (;;) {
13
- if (await stat(join(current, marker)).then(() => true, () => false)) return current;
14
- const parent = dirname(current);
15
- if (parent === current) return void 0;
16
- current = parent;
17
- }
18
- }
19
- function abbreviateHome(path) {
20
- const home = homedir();
21
- return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
22
- }
23
- /**
24
- * 工作区指令链:用户全局 `$DSH_HOME/AGENTS.md`,再从项目根到 cwd 的每一级目录取
25
- * 基础文件与本地 overlay。顺序由宽泛到具体——具体的在后面,与"更具体的优先"一致。
26
- */
27
- async function instructionChain(cwd, options) {
28
- const files = [];
29
- const global = join(options.dshHome, "AGENTS.md");
30
- if (await isFile(global)) files.push({
31
- root: options.dshHome,
32
- display: abbreviateHome(global),
33
- path: global
34
- });
35
- const root = await projectRoot(cwd) ?? resolve(cwd);
36
- const levels = [];
37
- let current = resolve(cwd);
38
- for (;;) {
39
- levels.unshift(current);
40
- if (current === root) break;
41
- const parent = dirname(current);
42
- if (parent === current) break;
43
- current = parent;
44
- }
45
- for (const level of levels) for (const name of [...options.instructionFileCandidates, ...options.localInstructionFileCandidates]) {
46
- const path = join(level, name);
47
- if (await isFile(path)) files.push({
48
- root,
49
- display: relativeDisplay(root, path),
50
- path
51
- });
52
- }
53
- return files;
54
- }
55
- function relativeDisplay(root, path) {
56
- const rel = path.slice(root.length + 1);
57
- return rel.length === 0 ? path : rel;
58
- }
59
- async function isFile(path) {
60
- return (await stat(path).then((value) => value, () => void 0))?.isFile() === true;
61
- }
62
- /** 读一个指令文件;按 `mtimeMs:size` 缓存,未变就不重读。 */
63
- async function readInstruction(file, cache, maxBytes) {
64
- const info = await stat(file.path).then((value) => value, () => void 0);
65
- if (info?.isFile() !== true) {
66
- cache.delete(file.path);
67
- return "";
68
- }
69
- const stamp = `${String(info.mtimeMs)}:${String(info.size)}`;
70
- const cached = cache.get(file.path);
71
- if (cached?.stamp === stamp) return cached.text;
72
- const text = budget(await readFile(file.path, "utf8").catch(() => ""), file.display, maxBytes);
73
- cache.set(file.path, {
74
- stamp,
75
- text
76
- });
77
- return text;
78
- }
79
- /** 单文件超出预算时截断并留一行可见提示(不静默丢内容)。 */
80
- function budget(text, display, maxBytes) {
81
- if (Buffer.byteLength(text, "utf8") <= maxBytes) return text.trimEnd();
82
- const kept = Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
83
- const lastBreak = kept.lastIndexOf("\n");
84
- return `${(lastBreak > 0 ? kept.slice(0, lastBreak) : kept).trimEnd()}\n\n(Workspace instruction budget ${String(maxBytes)} bytes: ${display} is truncated.)`;
85
- }
86
- /** 上游那行的身份基线:与 `packages/bundle/base/cordis.patch.yml` 的 `agent-instructions` 行 config 同源。 */
87
- const UPSTREAM_BASELINE_CONFIG = { maxBytes: 65536 };
88
- /** schema 调用会把缺省项补齐,所以这里拿到的是上游 schema 的实际默认值。 */
89
- function resolveBaselineConfig() {
90
- return Config({ ...UPSTREAM_BASELINE_CONFIG });
91
- }
92
- /** 上游 `findProjectRoot` 的语义:向上找第一个带标记的目录,找不到就用 cwd。 */
93
- async function projectRootOf(cwd, markers) {
94
- let current = resolve(cwd);
95
- for (;;) {
96
- for (const marker of markers) if (await exists(join(current, marker))) return current;
97
- const parent = dirname(current);
98
- if (parent === current) return resolve(cwd);
99
- current = parent;
100
- }
101
- }
102
- async function exists(path) {
103
- try {
104
- await stat(path);
105
- return true;
106
- } catch {
107
- return false;
108
- }
109
- }
110
- /** 一个会话的 baseline 身份;cwd 或项目根变了就是另一份身份(上游语义)。 */
111
- async function baselineIdentity(cwd) {
112
- const config = resolveBaselineConfig();
113
- const projectRoot = await projectRootOf(cwd, config.projectRootMarkers);
114
- return JSON.stringify({
115
- projectRoot: relative(cwd, projectRoot),
116
- projectRootMarkers: config.projectRootMarkers,
117
- maxBytes: config.maxBytes,
118
- maxSourceBytes: config.maxSourceBytes,
119
- instructionFileCandidates: config.instructionFileCandidates,
120
- localInstructionFileCandidates: config.localInstructionFileCandidates
121
- });
122
- }
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
158
- //#region src/agent-instructions/index.ts
159
- var agent_instructions_exports = /* @__PURE__ */ __exportAll({
160
- Config: () => Config$1,
161
- apply: () => apply,
162
- inject: () => inject,
163
- name: () => name
164
- });
165
- const name = "context-agent-instructions";
166
- const inject = ["agents", "contextAssembler"];
167
- const Config$1 = z.object({
168
- instructionFileCandidates: z.array(z.string()).default(["AGENTS.md"]),
169
- localInstructionFileCandidates: z.array(z.string()).default(["AGENTS.local.md"]),
170
- maxBytes: z.number().default(65536),
171
- dshHome: z.string().default(process.env["DSH_HOME"] ?? "")
172
- });
173
- /** 一个会话的指令链:路径集合与它所属的 cwd。 */
174
- const chains = /* @__PURE__ */ new WeakMap();
175
- /**
176
- * 工作区指令:一条文件一个规则块(id 为 `agent-instructions:<根标识>:<文件>`),文件变化时正文变、按 id 覆盖。
177
- *
178
- * 与上游 `agent-instructions` 的差别:不做 read/write/edit 的 touch 跟踪(本部署的 `AGENTS.md`
179
- * 几乎不变),只在每步按 `mtime:size` 对账;一份文件一条 id,于是变化只重发变了的那一份。
180
- */
181
- function apply(ctx, config) {
182
- const options = {
183
- instructionFileCandidates: config.instructionFileCandidates ?? ["AGENTS.md"],
184
- localInstructionFileCandidates: config.localInstructionFileCandidates ?? ["AGENTS.local.md"],
185
- dshHome: config.dshHome === void 0 || config.dshHome === "" ? defaultDshHome() : config.dshHome
186
- };
187
- const maxBytes = config.maxBytes ?? 65536;
188
- const cache = /* @__PURE__ */ new Map();
189
- const install = async (agent) => {
190
- if (chains.has(agent)) return;
191
- const cwd = agent.session.header.cwd === void 0 ? process.cwd() : agent.session.header.cwd;
192
- const chain = {
193
- cwd,
194
- files: await instructionChain(cwd, options)
195
- };
196
- chains.set(agent, chain);
197
- const identity = await baselineIdentity(cwd);
198
- for (const file of chain.files) ctx.contextAssembler.registerRule({
199
- id: `agent-instructions:${rootTag(file.root)}:${file.display}`,
200
- source: () => ({
201
- kind: "agent-instructions",
202
- form: "instructions",
203
- baseline: true,
204
- baselineIdentity: identity,
205
- changes: []
206
- }),
207
- text: async (target) => {
208
- if (await presetOwnsInstructions(ctx, target)) return "";
209
- const current = chains.get(target);
210
- if (current === void 0 || !current.files.some((entry) => entry.path === file.path)) return "";
211
- return readInstruction(file, cache, maxBytes);
212
- }
213
- });
214
- };
215
- ctx.on("agent/created", ({ agent }) => {
216
- install(agent).catch((error) => {
217
- ctx.logger.warn(`context-agent-instructions: ${String(error)}`);
218
- });
219
- });
220
- for (const agent of ctx.agents.list()) install(agent).catch((error) => {
221
- ctx.logger.warn(`context-agent-instructions: ${String(error)}`);
222
- });
223
- }
224
- function defaultDshHome() {
225
- const home = process.env["HOME"] ?? "~";
226
- return resolve(process.env["DSH_HOME"] ?? `${home}/.dsh`);
227
- }
228
- /**
229
- * 规则块 id 里的根标识:规则声明是**全局按 id 覆盖**的,而 `display` 只是根内相对路径——
230
- * 同进程里两个项目根都有 `AGENTS.md` 时,不带根标识的 id 会互相顶掉(后注册者覆盖前者,
231
- * 前者此后读到空正文)。8 位摘要够唯一,又不把绝对路径塞进提示词 id。
232
- */
233
- function rootTag(root) {
234
- return createHash("sha256").update(root).digest("hex").slice(0, 8);
235
- }
236
- //#endregion
237
- export { name as a, inject as i, agent_instructions_exports as n, apply as r, Config$1 as t };
@@ -1,27 +0,0 @@
1
- import z from "@deepseek-ai/schemastery";
2
- import { Context } from "@deepseek-ai/cordis";
3
- declare namespace index_d_exports {
4
- export { Config, apply, inject, name };
5
- }
6
- declare const name = "context-agent-instructions";
7
- declare const inject: string[];
8
- interface Config {
9
- /** 每级目录的基础指令文件名。 */
10
- instructionFileCandidates?: string[];
11
- /** 基础文件之后加载的本地 overlay 名。 */
12
- localInstructionFileCandidates?: string[];
13
- /** 单个指令文件的渲染上限(字节)。 */
14
- maxBytes?: number;
15
- /** 用户全局指令所在目录(默认 `$DSH_HOME` 或 `~/.dsh`)。 */
16
- dshHome?: string;
17
- }
18
- declare const Config: z<Config>;
19
- /**
20
- * 工作区指令:一条文件一个规则块(id 为 `agent-instructions:<根标识>:<文件>`),文件变化时正文变、按 id 覆盖。
21
- *
22
- * 与上游 `agent-instructions` 的差别:不做 read/write/edit 的 touch 跟踪(本部署的 `AGENTS.md`
23
- * 几乎不变),只在每步按 `mtime:size` 对账;一份文件一条 id,于是变化只重发变了的那一份。
24
- */
25
- declare function apply(ctx: Context, config: Config): void;
26
- //#endregion
27
- export { Config, apply, inject, name, index_d_exports as t };
@@ -1,2 +0,0 @@
1
- import { a as name, i as inject, r as apply, t as Config } from "./agent-instructions-CIgUVIeo.mjs";
2
- export { Config, apply, inject, name };
@@ -1,421 +0,0 @@
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`),幂等必须照样认。
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("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;");
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
- /**
93
- * 不要 instruction 类注入的会话(模式说了 `instructions: false`):常驻正文、规则块与降级 section 都不进。
94
- * 按需 skill 不受它管(那是模型自己加载),引用材料也不经通道。
95
- */
96
- withoutInstructions = /* @__PURE__ */ new WeakSet();
97
- /** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
98
- toolScopes = /* @__PURE__ */ new WeakMap();
99
- /** `host` 是构造期那个根 ctx:`skills` 的访问权限挂在插件的 inject 声明上,不能依赖访问者 ctx。 */
100
- constructor(host) {
101
- super(host, "contextAssembler");
102
- this.host = host;
103
- }
104
- registerSkill(declaration) {
105
- this.declarations.set(declaration.name, declaration);
106
- this.refreshSkills();
107
- return () => {
108
- if (this.declarations.get(declaration.name) !== declaration) return;
109
- this.declarations.delete(declaration.name);
110
- this.refreshSkills();
111
- };
112
- }
113
- registerRule(declaration) {
114
- this.rules.set(declaration.id, declaration);
115
- return () => {
116
- if (this.rules.get(declaration.id) === declaration) this.rules.delete(declaration.id);
117
- };
118
- }
119
- replaceSection(name, text) {
120
- this.overrides.set(name, text);
121
- return () => {
122
- if (this.overrides.get(name) === text) this.overrides.delete(name);
123
- };
124
- }
125
- suppressSection(name) {
126
- this.suppressed.add(name);
127
- return () => {
128
- this.suppressed.delete(name);
129
- };
130
- }
131
- isSuppressed(name) {
132
- return this.suppressed.has(name);
133
- }
134
- /** 装配结果上该 section 要换成的文本;undefined 表示原样保留。 */
135
- replacement(name, agent) {
136
- const override = this.overrides.get(name);
137
- return override === void 0 || agent === void 0 ? void 0 : override(agent);
138
- }
139
- /**
140
- * 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
141
- * `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
142
- */
143
- /** 这个会话要不要 instruction 类注入(模式说了 `instructions: false` 就是不要,见 {@link withoutInstructions})。 */
144
- setInstructions(agent, on) {
145
- if (on) this.withoutInstructions.delete(agent);
146
- else this.withoutInstructions.add(agent);
147
- }
148
- /**
149
- * 本会话的**工具收窄**(模式白名单):投影层只过滤装配结果、不碰工具注册表,所以"谁能用"这件事
150
- * 必须在通道上登记,技能目录与组正文才收得住。与 {@link setInstructions} 同形——谓词由模式那一行给
151
- * (`context-scope`),通道自己不碰 `tools` 服务。
152
- */
153
- restrictTools(agent, allowed) {
154
- this.toolScopes.set(agent, allowed);
155
- }
156
- /**
157
- * 本会话的工具可见性:调用方给出的注册表可见性 × 已登记的模式收窄。技能目录与组正文共用它,
158
- * 免得同一套判据在几处各写一遍。
159
- */
160
- visibleTools(agent, registered) {
161
- const allowed = this.toolScopes.get(agent);
162
- return allowed === void 0 ? registered : (tool) => registered(tool) && allowed(tool);
163
- }
164
- /**
165
- * 该会话看不到的 skill 名:声明了 `requires` 而依赖的工具一个都不可见。
166
- *
167
- * 目录由 `context-skill-catalog` 渲染,它把工具可见性作为谓词传进来(通道不碰 tools 服务)。
168
- */
169
- hiddenSkills(visible) {
170
- const hidden = /* @__PURE__ */ new Set();
171
- for (const [name, declaration] of this.declarations) {
172
- const requires = declaration.requires;
173
- if (requires === void 0 || requires.length === 0) continue;
174
- if (!requires.some(visible)) hidden.add(name);
175
- }
176
- return hidden;
177
- }
178
- /**
179
- * 该 skill 按会话修剪后的正文:注册表只能存一份(技能注册是装配期一次、全局可见),所以按需加载
180
- * 路径(接管的 `skill` 工具)要用这里重新算一次,才和 `auto` 正文一样跟着工具走。
181
- */
182
- contentFor(name, agent) {
183
- return this.declarations.get(name)?.content(agent);
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
- }
192
- async collect(agent) {
193
- const entries = /* @__PURE__ */ new Map();
194
- const scope = this.toolScopes.get(agent);
195
- const instructionsOff = this.withoutInstructions.has(agent);
196
- if (!instructionsOff) for (const declaration of this.declarations.values()) {
197
- if ((declaration.injection ?? "on-demand") !== "auto") continue;
198
- if (!this.reachable(declaration, scope)) continue;
199
- const body = declaration.content(agent);
200
- if (body.length > 0) entries.set(declaration.name, { text: renderVirtualSkill(declaration.name, body) });
201
- }
202
- for (const declaration of this.rules.values()) {
203
- if (instructionsOff) break;
204
- const text = await withTimeout(declaration.text(agent), CONTENT_TIMEOUT_MS);
205
- if (text.length > 0) entries.set(declaration.id, {
206
- text: renderReminder(declaration.id, text),
207
- ...declaration.source === void 0 ? {} : { source: declaration.source(agent) }
208
- });
209
- }
210
- if (!instructionsOff) for (const [id, text] of this.sectionsOf(agent)) entries.set(id, { text: renderReminder(id, text) });
211
- return entries;
212
- }
213
- sectionsOf(agent) {
214
- return this.states.get(agent)?.sections ?? /* @__PURE__ */ new Map();
215
- }
216
- /**
217
- * 每步装配调一次:记下本步降级出来的 section 文本,供下一步注入。
218
- */
219
- sync(agent, input) {
220
- this.stateOf(agent).sections = input.sections;
221
- }
222
- /** skill 注册(装配期一次,全局可见):正文没变的不重注册。 */
223
- refreshSkills(agent) {
224
- for (const [name, declaration] of this.declarations) {
225
- const content = declaration.content(agent);
226
- const modelInvocable = (declaration.injection ?? "on-demand") === "on-demand";
227
- const digest = `${modelInvocable ? "model" : "auto"}\u0000${declaration.description}\u0000${content}`;
228
- const current = this.skills.get(name);
229
- if (current?.digest === digest) continue;
230
- current?.dispose();
231
- const dispose = this.host.skills.register({
232
- name,
233
- description: declaration.description,
234
- content,
235
- source: "runtime",
236
- invocation: {
237
- modelInvocable,
238
- userInvocable: true
239
- }
240
- });
241
- this.skills.set(name, {
242
- digest,
243
- dispose
244
- });
245
- }
246
- for (const [name, registered] of this.skills) {
247
- if (this.declarations.has(name)) continue;
248
- registered.dispose();
249
- this.skills.delete(name);
250
- }
251
- }
252
- stateOf(agent) {
253
- const existing = this.states.get(agent);
254
- if (existing !== void 0) return existing;
255
- const state = { sections: /* @__PURE__ */ new Map() };
256
- this.states.set(agent, state);
257
- return state;
258
- }
259
- };
260
- /** 一条注入内容的取用上限:超过就按"没有内容"处理,不阻塞这一轮请求。 */
261
- const CONTENT_TIMEOUT_MS = 2e3;
262
- async function withTimeout(work, ms) {
263
- if (typeof work === "string") return work;
264
- const settled = Promise.resolve(work).catch(() => "");
265
- let timer;
266
- const expiry = new Promise((resolve) => {
267
- timer = setTimeout(() => {
268
- resolve("");
269
- }, ms);
270
- });
271
- try {
272
- return await Promise.race([settled, expiry]);
273
- } finally {
274
- if (timer !== void 0) clearTimeout(timer);
275
- }
276
- }
277
- //#endregion
278
- //#region src/assembler/defaults.ts
279
- /**
280
- * 留在系统提示词里的 section:部署级 persona(子 agent 在 agent 作用域注册的同名 persona 也命中)
281
- * 与覆盖规则声明([`rules.ts`](./rules.ts))。
282
- */
283
- const DEFAULT_KEEP = [
284
- PERSONA_PREFIX_SECTION,
285
- PERSONA_SUFFIX_SECTION,
286
- RULES_SECTION
287
- ];
288
- /**
289
- * 不进提示词的 section。三类:
290
- *
291
- * 1. **harness 自带的开场白与平台运维说明**:`harness:identity`("You are an AI agent powered by
292
- * DeepSeek Harness."——身份由各模式的 persona 给,这句话在本部署里是噪音,且是英文)、
293
- * `harness:source`(讲 dev server / HMR / 本地 checkout 位置);
294
- * 2. 本部署不装配的工具的用法说明:`tool:lsp` / `tool:pty` / `tool:session-query` / `tool:cordis` /
295
- * `tool:ralph`;
296
- * 3. `app:web-surface`(平台外壳说明)。
297
- *
298
- * 其余工具说明由各组的 `drops` 摘掉、要点由 toolkit 的 `guidance` 出口写进组正文,不在这一份里。
299
- */
300
- const DEFAULT_SUPPRESS = [
301
- "harness:identity",
302
- "harness:source",
303
- "app:web-surface",
304
- "tool:lsp",
305
- "tool:pty",
306
- "tool:session-query",
307
- "tool:cordis",
308
- "tool:ralph"
309
- ];
310
- /**
311
- * 改写成中文的两段上游说明(内容忠实原意):`@` 引用路径的语义、输出里的文件链接规范。
312
- *
313
- * 这两段注册在 agent 层(`context:file-reference`)与 host 层(`ui:deliverable-file-references`),
314
- * 注册层不归我们——所以在装配结果上换文本(不移除:内容要保留)。
315
- */
316
- const DEFAULT_REPLACE = {
317
- "context:file-reference": "以 @ 开头的词是用户显式引用的工作区路径,相对于工作区根。结尾带斜杠表示目录:需要时列目录内容。其余是文件:需要内容时用 read 工具读取,不要在读取前声称自己已经看过。用 @\"...\" 括住含空格的路径。",
318
- "ui:deliverable-file-references": "成功创建或修改文件后,在最终回复里点明主要产物。除命令、配置表达式与代码块之外,每处提到已存在的文件都要链到它的完整路径(相对工作目录或绝对路径);已知行号时在目标后加 `#L24` 或 `#L24-L30`。链接文本用文件名或清晰别名,只加够区分的父目录,不要把完整路径放进标签;默认只用文件名,需要精确位置时在标签后接 `:24` 或 `:24–30`(不带 `#` 或 `L`)。"
319
- };
320
- //#endregion
321
- //#region src/assembler/rules.ts
322
- /**
323
- * 覆盖规则的声明文本:写进**系统提示词**(不在 reminder 里重复),所以每条 reminder 只带 id 与正文。
324
- *
325
- * 为什么放系统提示词而不是首条 reminder:它是固定文本(不随会话变,不付缓存代价),而且放在这里
326
- * 才是系统级规则该在的位置——首条 reminder 自身会被压缩、被覆盖,权威性更低。
327
- */
328
- const RULES_TEXT = ["带 id 的 system-reminder 块是本系统提示词的一部分,持续有效:同 id 只有最新一条生效并取代更早的,不同 id 互不影响。", "skill_content 是要遵循的技能说明(随提示送来的与你后来加载的一样),file_content 是本步引用的文件内容。"].join("");
329
- //#endregion
330
- //#region src/assembler/index.ts
331
- var assembler_exports = /* @__PURE__ */ __exportAll({
332
- Config: () => Config,
333
- ContextAssembler: () => ContextAssembler,
334
- DEFAULT_KEEP: () => DEFAULT_KEEP,
335
- DEFAULT_REPLACE: () => DEFAULT_REPLACE,
336
- DEFAULT_SUPPRESS: () => DEFAULT_SUPPRESS,
337
- apply: () => apply,
338
- inject: () => inject,
339
- name: () => name,
340
- renderVirtualSkill: () => renderVirtualSkill
341
- });
342
- const name = "context-assembler";
343
- /** 规则声明的位置:紧跟部署 persona(order 0),在任何降级内容之前。 */
344
- const RULES_ORDER = 1;
345
- const inject = ["systemPrompt", "skills"];
346
- const Config = z.object({
347
- keep: z.array(z.string()).default([...DEFAULT_KEEP]),
348
- suppress: z.array(z.string()).default([...DEFAULT_SUPPRESS]),
349
- replace: z.dict(z.string()).default({ ...DEFAULT_REPLACE })
350
- });
351
- /**
352
- * 提示词注入的唯一通道:system prompt 里只留 `keep`,其余内容按声明的方式到达模型——
353
- * 降级为紧随用户消息的 reminder、或直接丢弃(写进按需 skill 正文是调用方自己的事)。
354
- */
355
- function apply(ctx, config) {
356
- const channel = new ContextAssembler(ctx);
357
- const keep = new Set(config.keep ?? DEFAULT_KEEP);
358
- for (const name of config.suppress ?? DEFAULT_SUPPRESS) channel.suppressSection(name);
359
- for (const [name, text] of Object.entries(config.replace ?? DEFAULT_REPLACE)) channel.replaceSection(name, () => text);
360
- ctx.systemPrompt.section({
361
- name: RULES_SECTION,
362
- order: RULES_ORDER,
363
- text: RULES_TEXT
364
- });
365
- ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
366
- const result = await next();
367
- const agent = context.agent;
368
- const demoted = /* @__PURE__ */ new Map();
369
- const sections = [];
370
- let changed = false;
371
- for (const section of result.sections) {
372
- if (channel.isSuppressed(section.name)) {
373
- changed = true;
374
- continue;
375
- }
376
- const text = channel.replacement(section.name, agent) ?? section.text;
377
- if (text !== section.text) changed = true;
378
- if (keep.has(section.name) || text.length === 0) {
379
- sections.push(text === section.text ? section : {
380
- ...section,
381
- text
382
- });
383
- continue;
384
- }
385
- changed = true;
386
- demoted.set(sectionId(section.name), text);
387
- }
388
- if (agent !== void 0) channel.sync(agent, { sections: demoted });
389
- return changed ? {
390
- ...result,
391
- sections
392
- } : result;
393
- });
394
- ctx.on("agent/pre-step", async ({ agent, messages }, next) => {
395
- const decision = await next();
396
- if (decision.kind === "reject" || decision.messages.length === 0) return decision;
397
- const pending = await pendingEntries(agent, channel);
398
- if (pending.length === 0) return decision;
399
- const claimedEnd = decision.messages.findLastIndex((message) => messages.includes(message));
400
- return {
401
- ...decision,
402
- messages: decision.messages.toSpliced(claimedEnd + 1, 0, ...pending.map(([key, entry]) => reminderMessage(key, entry.text, entry.source)))
403
- };
404
- });
405
- ctx.on("agent/request-error", async ({ agent, signal }, next) => {
406
- const action = await next();
407
- if (action?.kind !== "retry" || signal.aborted) return action;
408
- for (const [key, entry] of await pendingEntries(agent, channel)) agent.session.append("user/message", reminderMessage(key, entry.text, entry.source), { surfaceOp: "append" });
409
- return action;
410
- }, { prepend: true });
411
- }
412
- /** 降级 section 的条目 id:一条 section 一个 id,于是同一次变化只重发那一条。 */
413
- function sectionId(name) {
414
- return `section:${name}`;
415
- }
416
- /** 本步要注入的条目:键相同且文本未变就不注入,按键字典序(顺序不随注册顺序抖动)。 */
417
- async function pendingEntries(agent, channel) {
418
- return [...await channel.collect(agent)].filter(([key, entry]) => latestReminderText(agent, key) !== entry.text).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
419
- }
420
- //#endregion
421
- 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 };