@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.
@@ -0,0 +1,117 @@
1
+ import { readFile, stat } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+
5
+ /** 一条要注入的指令文件:`display` 进 id 与正文,`path` 用来读,`root` 区分同名文件。 */
6
+ export interface InstructionFile {
7
+ /** 该文件所属的根(项目根或 `$DSH_HOME`):规则块 id 靠它区分不同根下的同名文件。 */
8
+ readonly root: string;
9
+ readonly display: string;
10
+ readonly path: string;
11
+ }
12
+
13
+ export interface WorkspaceOptions {
14
+ readonly instructionFileCandidates: readonly string[];
15
+ readonly localInstructionFileCandidates: readonly string[];
16
+ readonly dshHome: string;
17
+ }
18
+
19
+ /** 从 cwd 逐级向上找项目根(含标记目录的那一级)。 */
20
+ async function projectRoot(cwd: string, marker = ".git"): Promise<string | undefined> {
21
+ let current = resolve(cwd);
22
+ for (;;) {
23
+ const found = await stat(join(current, marker)).then(
24
+ () => true,
25
+ () => false,
26
+ );
27
+ if (found) return current;
28
+ const parent = dirname(current);
29
+ if (parent === current) return undefined;
30
+ current = parent;
31
+ }
32
+ }
33
+
34
+ function abbreviateHome(path: string): string {
35
+ const home = homedir();
36
+ return path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
37
+ }
38
+
39
+ /**
40
+ * 工作区指令链:用户全局 `$DSH_HOME/AGENTS.md`,再从项目根到 cwd 的每一级目录取
41
+ * 基础文件与本地 overlay。顺序由宽泛到具体——具体的在后面,与"更具体的优先"一致。
42
+ */
43
+ export async function instructionChain(
44
+ cwd: string,
45
+ options: WorkspaceOptions,
46
+ ): Promise<InstructionFile[]> {
47
+ const files: InstructionFile[] = [];
48
+ const global = join(options.dshHome, "AGENTS.md");
49
+ if (await isFile(global))
50
+ files.push({ root: options.dshHome, display: abbreviateHome(global), path: global });
51
+
52
+ const root = (await projectRoot(cwd)) ?? resolve(cwd);
53
+ const levels: string[] = [];
54
+ let current = resolve(cwd);
55
+ for (;;) {
56
+ levels.unshift(current);
57
+ if (current === root) break;
58
+ const parent = dirname(current);
59
+ if (parent === current) break;
60
+ current = parent;
61
+ }
62
+ for (const level of levels) {
63
+ for (const name of [
64
+ ...options.instructionFileCandidates,
65
+ ...options.localInstructionFileCandidates,
66
+ ]) {
67
+ const path = join(level, name);
68
+ if (await isFile(path)) files.push({ root, display: relativeDisplay(root, path), path });
69
+ }
70
+ }
71
+ return files;
72
+ }
73
+
74
+ function relativeDisplay(root: string, path: string): string {
75
+ const rel = path.slice(root.length + 1);
76
+ return rel.length === 0 ? path : rel;
77
+ }
78
+
79
+ async function isFile(path: string): Promise<boolean> {
80
+ const info = await stat(path).then(
81
+ (value) => value,
82
+ () => undefined,
83
+ );
84
+ return info?.isFile() === true;
85
+ }
86
+
87
+ /** 读一个指令文件;按 `mtimeMs:size` 缓存,未变就不重读。 */
88
+ export async function readInstruction(
89
+ file: InstructionFile,
90
+ cache: Map<string, { stamp: string; text: string }>,
91
+ maxBytes: number,
92
+ ): Promise<string> {
93
+ const info = await stat(file.path).then(
94
+ (value) => value,
95
+ () => undefined,
96
+ );
97
+ if (info?.isFile() !== true) {
98
+ cache.delete(file.path);
99
+ return "";
100
+ }
101
+ const stamp = `${String(info.mtimeMs)}:${String(info.size)}`;
102
+ const cached = cache.get(file.path);
103
+ if (cached?.stamp === stamp) return cached.text;
104
+ const raw = await readFile(file.path, "utf8").catch(() => "");
105
+ const text = budget(raw, file.display, maxBytes);
106
+ cache.set(file.path, { stamp, text });
107
+ return text;
108
+ }
109
+
110
+ /** 单文件超出预算时截断并留一行可见提示(不静默丢内容)。 */
111
+ function budget(text: string, display: string, maxBytes: number): string {
112
+ if (Buffer.byteLength(text, "utf8") <= maxBytes) return text.trimEnd();
113
+ const kept = Buffer.from(text, "utf8").subarray(0, maxBytes).toString("utf8");
114
+ const lastBreak = kept.lastIndexOf("\n");
115
+ const body = (lastBreak > 0 ? kept.slice(0, lastBreak) : kept).trimEnd();
116
+ return `${body}\n\n(Workspace instruction budget ${String(maxBytes)} bytes: ${display} is truncated.)`;
117
+ }
@@ -0,0 +1,107 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import type { Context } from "@deepseek-ai/cordis";
4
+ import type { Agent } from "@deepseek-ai/dsh-agent";
5
+ import type {} from "@deepseek-ai/dsh-agent-instructions";
6
+ import z from "@deepseek-ai/schemastery";
7
+ import type {} from "../assembler/index.ts";
8
+ import { instructionChain, readInstruction, type InstructionFile } from "./files.ts";
9
+ import { baselineIdentity } from "./baseline.ts";
10
+
11
+ export const name = "context-agent-instructions";
12
+
13
+ export const inject = ["agents", "contextAssembler"];
14
+
15
+ export interface Config {
16
+ /** 每级目录的基础指令文件名。 */
17
+ instructionFileCandidates?: string[];
18
+ /** 基础文件之后加载的本地 overlay 名。 */
19
+ localInstructionFileCandidates?: string[];
20
+ /** 单个指令文件的渲染上限(字节)。 */
21
+ maxBytes?: number;
22
+ /** 用户全局指令所在目录(默认 `$DSH_HOME` 或 `~/.dsh`)。 */
23
+ dshHome?: string;
24
+ }
25
+
26
+ export const Config: z<Config> = z.object({
27
+ instructionFileCandidates: z.array(z.string()).default(["AGENTS.md"]),
28
+ localInstructionFileCandidates: z.array(z.string()).default(["AGENTS.local.md"]),
29
+ maxBytes: z.number().default(65536),
30
+ dshHome: z.string().default(process.env["DSH_HOME"] ?? ""),
31
+ });
32
+
33
+ /** 一个会话的指令链:路径集合与它所属的 cwd。 */
34
+ const chains = new WeakMap<Agent, { cwd: string; files: InstructionFile[] }>();
35
+
36
+ /**
37
+ * 工作区指令:一条文件一个规则块(id 为 `agent-instructions:<根标识>:<文件>`),文件变化时正文变、按 id 覆盖。
38
+ *
39
+ * 与上游 `agent-instructions` 的差别:不做 read/write/edit 的 touch 跟踪(本部署的 `AGENTS.md`
40
+ * 几乎不变),只在每步按 `mtime:size` 对账;一份文件一条 id,于是变化只重发变了的那一份。
41
+ */
42
+ export function apply(ctx: Context, config: Config): void {
43
+ const options = {
44
+ instructionFileCandidates: config.instructionFileCandidates ?? ["AGENTS.md"],
45
+ localInstructionFileCandidates: config.localInstructionFileCandidates ?? ["AGENTS.local.md"],
46
+ dshHome:
47
+ config.dshHome === undefined || config.dshHome === "" ? defaultDshHome() : config.dshHome,
48
+ };
49
+ const maxBytes = config.maxBytes ?? 65536;
50
+ const cache = new Map<string, { stamp: string; text: string }>();
51
+
52
+ const install = async (agent: Agent): Promise<void> => {
53
+ if (chains.has(agent)) return;
54
+ const cwd = agent.session.header.cwd === undefined ? process.cwd() : agent.session.header.cwd;
55
+ const chain = { cwd, files: await instructionChain(cwd, options) };
56
+ chains.set(agent, chain);
57
+ // 上游认领判据要的身份:缺它,上游(官方 preset 自己装的那行)会以为基线不存在,再注入一条自己的模板。
58
+ const identity = await baselineIdentity(cwd);
59
+ for (const file of chain.files) {
60
+ ctx.contextAssembler.registerRule({
61
+ id: `agent-instructions:${rootTag(file.root)}:${file.display}`,
62
+ // 对外身份沿用上游那两样:kind 让客户端标签与按 kind 认领的消费方(上游的实验性约束收集)认得这是
63
+ // 工作区指令;`baseline` 与 `baselineIdentity` 让上游的认领判据(kind + baseline===true + 身份相等)
64
+ // 认这份条目就是基线,于是它不再注入自己那条模板。`changes` 留空——上游那套按文件做 reconciliation
65
+ // 的记录我们不做(一条文件一条 id)。
66
+ source: () => ({
67
+ kind: "agent-instructions",
68
+ form: "instructions",
69
+ baseline: true,
70
+ baselineIdentity: identity,
71
+ changes: [],
72
+ }),
73
+ text: (target) => {
74
+ const current = chains.get(target);
75
+ if (current === undefined || !current.files.some((entry) => entry.path === file.path))
76
+ return "";
77
+ return readInstruction(file, cache, maxBytes);
78
+ },
79
+ });
80
+ }
81
+ };
82
+
83
+ ctx.on("agent/created", ({ agent }) => {
84
+ void install(agent).catch((error: unknown) => {
85
+ ctx.logger.warn(`context-agent-instructions: ${String(error)}`);
86
+ });
87
+ });
88
+ for (const agent of ctx.agents.list()) {
89
+ void install(agent).catch((error: unknown) => {
90
+ ctx.logger.warn(`context-agent-instructions: ${String(error)}`);
91
+ });
92
+ }
93
+ }
94
+
95
+ function defaultDshHome(): string {
96
+ const home = process.env["HOME"] ?? "~";
97
+ return resolve(process.env["DSH_HOME"] ?? `${home}/.dsh`);
98
+ }
99
+
100
+ /**
101
+ * 规则块 id 里的根标识:规则声明是**全局按 id 覆盖**的,而 `display` 只是根内相对路径——
102
+ * 同进程里两个项目根都有 `AGENTS.md` 时,不带根标识的 id 会互相顶掉(后注册者覆盖前者,
103
+ * 前者此后读到空正文)。8 位摘要够唯一,又不把绝对路径塞进提示词 id。
104
+ */
105
+ function rootTag(root: string): string {
106
+ return createHash("sha256").update(root).digest("hex").slice(0, 8);
107
+ }
@@ -0,0 +1,285 @@
1
+ import type { Agent } from "@deepseek-ai/dsh-agent";
2
+ import { Service, type Context } from "@deepseek-ai/cordis";
3
+ import type { MessageSource } from "@deepseek-ai/dsh-llm";
4
+ import { renderReminder, renderVirtualSkill } from "./reminder.ts";
5
+
6
+ /** 正文到达模型的方式:自动注入,或等模型按需加载。 */
7
+ export type InjectionMode = "auto" | "on-demand";
8
+
9
+ /**
10
+ * 一条按 skill 形态声明的注入:正文是本仓库维护的文案。
11
+ *
12
+ * 正文与 agent 无关(工具集在装配期就定了),所以 skill 在装配期注册一次、全局可见。
13
+ */
14
+ export interface PromptSkillDeclaration {
15
+ /** kebab-case:skill 名,也是注入条目的 id。 */
16
+ readonly name: string;
17
+ readonly title: string;
18
+ /** 目录行摘要:讲清什么时候该加载它。 */
19
+ readonly description: string;
20
+ /**
21
+ * 正文按**会话**生成:模式可能只给一部分工具,正文要跟着修剪(不该讲用不到的工具)。
22
+ * 不传 agent(装配期注册 skill 时)返回不过滤的完整版。
23
+ */
24
+ readonly content: (agent?: Agent) => string;
25
+ /**
26
+ * 这个 skill 赖以成立的**入口工具**(任一存在即可,不是全部)。都不可见时它不该出现在该会话的技能
27
+ * 目录里——"不能用 skill 工具就没有技能目录"是同一个道理:技能也跟着工具走。
28
+ *
29
+ * 缺省语义就是"一个都不在就藏起来";声明方要自己挑入口,别把组内所有可能的工具都倒进来
30
+ * (子代理控制行与团队插件会提供同名工具,全集当判据会把不该出现的 skill 留下)。
31
+ */
32
+ readonly requires?: readonly string[];
33
+ /** 缺省 `on-demand`:只有正文确有必要常驻时才写 `auto`。 */
34
+ readonly injection?: InjectionMode;
35
+ }
36
+
37
+ interface RegisteredSkill {
38
+ readonly digest: string;
39
+ readonly dispose: () => void;
40
+ }
41
+
42
+ interface AgentState {
43
+ /** 本步装配降级出来的 section 条目:id → 正文(未渲染信封)。 */
44
+ sections: ReadonlyMap<string, string>;
45
+ }
46
+
47
+ /**
48
+ * 一条规则块声明:系统级信息(工作区指令、skill 目录),文本按 id 幂等注入、同 id 覆盖。
49
+ * 文本可以是异步的(skill 目录要读 skill 注册表)。
50
+ */
51
+ export interface PromptRuleDeclaration {
52
+ readonly id: string;
53
+ readonly text: (agent: Agent) => string | Promise<string>;
54
+ /**
55
+ * 这条规则的对外身份。接管上游那两面(工作区指令 / skill 目录)按上游 kind 发消息,好让按 kind 认领的
56
+ * 消费方(客户端标签、上游的实验性约束收集)认得出来;不声明就是通道自己的 `context-assembler`。
57
+ * 幂等键仍是 `id`(`reminderMessage` 会把它塞进 source)。
58
+ */
59
+ readonly source?: (agent: Agent) => MessageSource;
60
+ }
61
+
62
+ /** 一条待注入的条目:正文已渲染好,`source` 是它声明给外部的身份。 */
63
+ export interface PromptEntry {
64
+ readonly text: string;
65
+ readonly source?: MessageSource;
66
+ }
67
+
68
+ /**
69
+ * 注入通道:调用方声明「什么内容、怎么到达模型」,这里负责到达方式。
70
+ *
71
+ * - `on-demand`:注册成模型可用 skill(进 skill 目录,正文按需加载);
72
+ * - `auto`:正文自动注入 reminder(注册成 skill 但标 `modelInvocable: false`,因为已经常驻);
73
+ * - `replaceSection` / `suppressSection`:装配结果上的文本改写与丢弃。
74
+ *
75
+ * 覆盖语义(同 id 最新取代更早)由系统提示词里的声明说明一次,见 [`rules.ts`](./rules.ts)。
76
+ */
77
+ export class ContextAssembler extends Service {
78
+ private readonly declarations = new Map<string, PromptSkillDeclaration>();
79
+ private readonly rules = new Map<string, PromptRuleDeclaration>();
80
+ private readonly overrides = new Map<string, (agent: Agent) => string | undefined>();
81
+ private readonly suppressed = new Set<string>();
82
+ private readonly skills = new Map<string, RegisteredSkill>();
83
+ private readonly states = new WeakMap<Agent, AgentState>();
84
+ /** 不要 instruction 类注入(规则块)的会话;内容块(技能正文、引用材料)不受它管。 */
85
+ private readonly withoutInstructions = new WeakSet<Agent>();
86
+
87
+ /** 本会话的模式收窄(白名单):投影层的过滤不碰注册表,注册表上看不出"谁能用"。 */
88
+ private readonly toolScopes = new WeakMap<Agent, (tool: string) => boolean>();
89
+
90
+ /** `host` 是构造期那个根 ctx:`skills` 的访问权限挂在插件的 inject 声明上,不能依赖访问者 ctx。 */
91
+ constructor(private readonly host: Context) {
92
+ super(host, "contextAssembler");
93
+ }
94
+
95
+ registerSkill(declaration: PromptSkillDeclaration): () => void {
96
+ this.declarations.set(declaration.name, declaration);
97
+ this.refreshSkills();
98
+ return () => {
99
+ if (this.declarations.get(declaration.name) !== declaration) return;
100
+ this.declarations.delete(declaration.name);
101
+ this.refreshSkills();
102
+ };
103
+ }
104
+
105
+ registerRule(declaration: PromptRuleDeclaration): () => void {
106
+ this.rules.set(declaration.id, declaration);
107
+ return () => {
108
+ if (this.rules.get(declaration.id) === declaration) this.rules.delete(declaration.id);
109
+ };
110
+ }
111
+
112
+ replaceSection(name: string, text: (agent: Agent) => string | undefined): () => void {
113
+ this.overrides.set(name, text);
114
+ return () => {
115
+ if (this.overrides.get(name) === text) this.overrides.delete(name);
116
+ };
117
+ }
118
+
119
+ suppressSection(name: string): () => void {
120
+ this.suppressed.add(name);
121
+ return () => {
122
+ this.suppressed.delete(name);
123
+ };
124
+ }
125
+
126
+ isSuppressed(name: string): boolean {
127
+ return this.suppressed.has(name);
128
+ }
129
+
130
+ /** 装配结果上该 section 要换成的文本;undefined 表示原样保留。 */
131
+ replacement(name: string, agent: Agent | undefined): string | undefined {
132
+ const override = this.overrides.get(name);
133
+ return override === undefined || agent === undefined ? undefined : override(agent);
134
+ }
135
+
136
+ /**
137
+ * 本步要注入的条目:键 → 已渲染好的正文(规则块或内容块)。
138
+ * `sections` 来自本步装配,`rules` 与 `auto` skill 正文在这里现算。
139
+ */
140
+ /** 这个会话不要 instruction 类的规则块(模式说了 `instructions: false`)。 */
141
+ setInstructions(agent: Agent, on: boolean): void {
142
+ if (on) this.withoutInstructions.delete(agent);
143
+ else this.withoutInstructions.add(agent);
144
+ }
145
+
146
+ /**
147
+ * 本会话的**工具收窄**(模式白名单):投影层只过滤装配结果、不碰工具注册表,所以"谁能用"这件事
148
+ * 必须在通道上登记,技能目录与组正文才收得住。与 {@link setInstructions} 同形——谓词由模式那一行给
149
+ * (`context-scope`),通道自己不碰 `tools` 服务。
150
+ */
151
+ restrictTools(agent: Agent, allowed: (tool: string) => boolean): void {
152
+ this.toolScopes.set(agent, allowed);
153
+ }
154
+
155
+ /**
156
+ * 本会话的工具可见性:调用方给出的注册表可见性 × 已登记的模式收窄。技能目录与组正文共用它,
157
+ * 免得同一套判据在几处各写一遍。
158
+ */
159
+ visibleTools(agent: Agent, registered: (tool: string) => boolean): (tool: string) => boolean {
160
+ const allowed = this.toolScopes.get(agent);
161
+ return allowed === undefined ? registered : (tool) => registered(tool) && allowed(tool);
162
+ }
163
+
164
+ /**
165
+ * 该会话看不到的 skill 名:声明了 `requires` 而依赖的工具一个都不可见。
166
+ *
167
+ * 目录由 `context-skill-catalog` 渲染,它把工具可见性作为谓词传进来(通道不碰 tools 服务)。
168
+ */
169
+ hiddenSkills(visible: (tool: string) => boolean): ReadonlySet<string> {
170
+ const hidden = new Set<string>();
171
+ for (const [name, declaration] of this.declarations) {
172
+ const requires = declaration.requires;
173
+ if (requires === undefined || requires.length === 0) continue;
174
+ if (!requires.some(visible)) hidden.add(name);
175
+ }
176
+ return hidden;
177
+ }
178
+
179
+ /**
180
+ * 该 skill 按会话修剪后的正文:注册表只能存一份(技能注册是装配期一次、全局可见),所以按需加载
181
+ * 路径(接管的 `skill` 工具)要用这里重新算一次,才和 `auto` 正文一样跟着工具走。
182
+ */
183
+ contentFor(name: string, agent: Agent | undefined): string | undefined {
184
+ return this.declarations.get(name)?.content(agent);
185
+ }
186
+
187
+ async collect(agent: Agent): Promise<Map<string, PromptEntry>> {
188
+ 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
+ }
196
+ const instructionsOff = this.withoutInstructions.has(agent);
197
+ for (const declaration of this.rules.values()) {
198
+ if (instructionsOff) break;
199
+ // 注入路径不能被任何一个内容提供者拖死:文本是异步算的(技能目录要读注册表、工作区指令要读文件),
200
+ // 谁卡住都不该让人等在这里——超时或抛错都按"这条没有内容"处理。
201
+ const text = await withTimeout(declaration.text(agent), CONTENT_TIMEOUT_MS);
202
+ if (text.length > 0) {
203
+ entries.set(declaration.id, {
204
+ text: renderReminder(declaration.id, text),
205
+ ...(declaration.source === undefined ? {} : { source: declaration.source(agent) }),
206
+ });
207
+ }
208
+ }
209
+ if (!instructionsOff) {
210
+ for (const [id, text] of this.sectionsOf(agent)) {
211
+ entries.set(id, { text: renderReminder(id, text) });
212
+ }
213
+ }
214
+ return entries;
215
+ }
216
+
217
+ private sectionsOf(agent: Agent): ReadonlyMap<string, string> {
218
+ return this.states.get(agent)?.sections ?? new Map();
219
+ }
220
+
221
+ /**
222
+ * 每步装配调一次:记下本步降级出来的 section 文本,供下一步注入。
223
+ */
224
+ sync(agent: Agent, input: { readonly sections: ReadonlyMap<string, string> }): void {
225
+ this.stateOf(agent).sections = input.sections;
226
+ }
227
+
228
+ /** skill 注册(装配期一次,全局可见):正文没变的不重注册。 */
229
+ private refreshSkills(agent?: Agent): void {
230
+ for (const [name, declaration] of this.declarations) {
231
+ const content = declaration.content(agent);
232
+ const modelInvocable = (declaration.injection ?? "on-demand") === "on-demand";
233
+ const digest = `${modelInvocable ? "model" : "auto"}\u0000${declaration.description}\u0000${content}`;
234
+ const current = this.skills.get(name);
235
+ if (current?.digest === digest) continue;
236
+ current?.dispose();
237
+ const dispose = this.host.skills.register({
238
+ name,
239
+ description: declaration.description,
240
+ content,
241
+ source: "runtime",
242
+ invocation: { modelInvocable, userInvocable: true },
243
+ });
244
+ this.skills.set(name, { digest, dispose });
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
+
253
+ private stateOf(agent: Agent): AgentState {
254
+ const existing = this.states.get(agent);
255
+ if (existing !== undefined) return existing;
256
+ const state: AgentState = { sections: new Map() };
257
+ this.states.set(agent, state);
258
+ return state;
259
+ }
260
+ }
261
+
262
+ declare module "@deepseek-ai/cordis" {
263
+ interface Context {
264
+ contextAssembler: ContextAssembler;
265
+ }
266
+ }
267
+
268
+ /** 一条注入内容的取用上限:超过就按"没有内容"处理,不阻塞这一轮请求。 */
269
+ const CONTENT_TIMEOUT_MS = 2000;
270
+
271
+ async function withTimeout(work: string | Promise<string>, ms: number): Promise<string> {
272
+ if (typeof work === "string") return work;
273
+ const settled = Promise.resolve(work).catch(() => "");
274
+ let timer: ReturnType<typeof setTimeout> | undefined;
275
+ const expiry = new Promise<string>((resolve) => {
276
+ timer = setTimeout(() => {
277
+ resolve("");
278
+ }, ms);
279
+ });
280
+ try {
281
+ return await Promise.race([settled, expiry]);
282
+ } finally {
283
+ if (timer !== undefined) clearTimeout(timer);
284
+ }
285
+ }
@@ -0,0 +1,53 @@
1
+ import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
2
+ import { RULES_SECTION } from "./reminder.ts";
3
+
4
+ /**
5
+ * 留在系统提示词里的 section:部署级 persona(子 agent 在 agent 作用域注册的同名 persona 也命中)
6
+ * 与覆盖规则声明([`rules.ts`](./rules.ts))。
7
+ */
8
+ export const DEFAULT_KEEP: readonly string[] = [
9
+ PERSONA_PREFIX_SECTION,
10
+ PERSONA_SUFFIX_SECTION,
11
+ RULES_SECTION,
12
+ ];
13
+
14
+ /**
15
+ * 不进提示词的 section。三类:
16
+ *
17
+ * 1. **harness 自带的开场白与平台运维说明**:`harness:identity`("You are an AI agent powered by
18
+ * DeepSeek Harness."——身份由各模式的 persona 给,这句话在本部署里是噪音,且是英文)、
19
+ * `harness:source`(讲 dev server / HMR / 本地 checkout 位置);
20
+ * 2. 本部署不装配的工具的用法说明:`tool:lsp` / `tool:pty` / `tool:session-query` / `tool:cordis` /
21
+ * `tool:ralph`;
22
+ * 3. `app:web-surface`(平台外壳说明)。
23
+ *
24
+ * 其余工具说明由各组的 `drops` 摘掉、要点由 toolkit 的 `guidance` 出口写进组正文,不在这一份里。
25
+ */
26
+ export const DEFAULT_SUPPRESS: readonly string[] = [
27
+ "harness:identity",
28
+ "harness:source",
29
+ "app:web-surface",
30
+ "tool:lsp",
31
+ "tool:pty",
32
+ "tool:session-query",
33
+ "tool:cordis",
34
+ "tool:ralph",
35
+ ];
36
+
37
+ /**
38
+ * 改写成中文的两段上游说明(内容忠实原意):`@` 引用路径的语义、输出里的文件链接规范。
39
+ *
40
+ * 这两段注册在 agent 层(`context:file-reference`)与 host 层(`ui:deliverable-file-references`),
41
+ * 注册层不归我们——所以在装配结果上换文本(不移除:内容要保留)。
42
+ */
43
+ export const DEFAULT_REPLACE: Readonly<Record<string, string>> = {
44
+ "context:file-reference":
45
+ "以 @ 开头的词是用户显式引用的工作区路径,相对于工作区根。结尾带斜杠表示目录:需要时列目录内容。" +
46
+ "其余是文件:需要内容时用 read 工具读取,不要在读取前声称自己已经看过。" +
47
+ '用 @"..." 括住含空格的路径。',
48
+ "ui:deliverable-file-references":
49
+ "成功创建或修改文件后,在最终回复里点明主要产物。除命令、配置表达式与代码块之外," +
50
+ "每处提到已存在的文件都要链到它的完整路径(相对工作目录或绝对路径);已知行号时在目标后加 " +
51
+ "`#L24` 或 `#L24-L30`。链接文本用文件名或清晰别名,只加够区分的父目录,不要把完整路径放进标签;" +
52
+ "默认只用文件名,需要精确位置时在标签后接 `:24` 或 `:24–30`(不带 `#` 或 `L`)。",
53
+ };