@morlay/dsh-prompt-reminder 0.0.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 morlay
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @morlay/dsh-prompt-reminder
2
+
3
+ 把系统提示词里被裁掉的 section 降级为 `<system-reminder>` user 消息的 cordis
4
+ 插件:系统提示词只留部署级 persona(`personaPrefix` / `personaSuffix`),工具
5
+ 说明、Agent Teams 规则、plan 规则、PTC SDK 文档等在每轮请求前随用户消息送达。
6
+
7
+ ## 为什么
8
+
9
+ 上游把每个工具的跨调用说明(`tool:bash`、`tool:read`、`tool:glob`、…)、Agent
10
+ Teams 协作规则、harness 源码位置、Web GUI 说明都注册成系统提示词的 section。
11
+ 它们条数多、篇幅大(本部署实测 9200 字符里约 7400),挤在上下文最前面稀释
12
+ 真正要遵守的少量纪律。本插件把这些 section 从系统提示词移到紧随用户消息之后
13
+ 的 reminder:内容一字不少,注意力分层干净;系统提示词也不再随 plan / PTC 模式
14
+ 切换而变动。
15
+
16
+ ## 行为
17
+
18
+ | 环节 | 做什么 |
19
+ | -------------------------------- | ------------------------------------------------------------------------------------ |
20
+ | `system-prompt/assemble`(瀑布) | 保留 `keep` 名单内的 section;其余非空 section 从 assembly 移除,并按 order 捕获文本 |
21
+ | `agent/pre-step` | 本轮文本与上一次送达的不同时,注入一条 user 消息,位置紧随已领取的这批消息之后 |
22
+
23
+ - **信封**:`<system-reminder>`,与工作区指令同一约定。首行声明系统提示词是刻意
24
+ 精简的、下面的内容仍属系统级规则、最新一条覆盖更早的 reminder。
25
+ - **首步一次 + 变化增量**:文本不变时不重复注入;`plan:policy`、`tools:ptc-only`、
26
+ `tools:sdk` 这类按模式渲染的 section 变化时追加一条新的**全量** reminder
27
+ (靠首行的覆盖声明消歧,不试图只发差量)。
28
+ - **幂等**:进程内按 agent 记最近送达文本;重启 / 恢复时扫会话 surface 上最近一条
29
+ 本插件消息,文本相同就不再注入。
30
+ - **转义**:正文里的 `</system-reminder>` 写成 `<\/system-reminder>`,避免提前闭合信封。
31
+
32
+ ## 配置
33
+
34
+ | 字段 | 默认 | 含义 |
35
+ | ------ | ------------------------------------------------------------ | ---------------------------------------------------------- |
36
+ | `keep` | `['deployment:persona-prefix', 'deployment:persona-suffix']` | 留在系统提示词里的 section 名;其余(非空)降级为 reminder |
37
+
38
+ `keep` 按 **section 名**匹配,所以子 agent 在 agent scope 注册的同名 persona 也
39
+ 自动留在系统提示词里。
40
+
41
+ ## 装配
42
+
43
+ 作为 **host plane** 的部署级行,在 profile 的 bundle patch 里定义一次即可:
44
+
45
+ ```yaml
46
+ - insert:
47
+ - id: prompt-reminder
48
+ name: "@morlay/dsh-prompt-reminder"
49
+ ```
50
+
51
+ `@morlay/dsh-preset` 的 `cordis.patch.yml` 就是这么声明的:一次覆盖全部 preset
52
+ (standard / ptc 以及后续新增的),preset composition 不各自带这一行。
53
+
54
+ profile 的依赖树必须能解析该包名(示例 app `apps/dsh-custom-next` 的
55
+ `dependencies` 已声明)——preset 的行与 host plane 的行都用 profile 的 resolver
56
+ 解析,换工作区时要一并声明,否则该行加载失败、工具说明会留在系统提示词里。
57
+
58
+ 放在 host plane 而不是 preset composition:这是一条与具体 preset 无关的部署级
59
+ 策略;注册在 root scope 的 listener 会收到每个 agent 的装配与 pre-step 事件。
60
+
61
+ ## 前提
62
+
63
+ - 系统提示词由部署级 `system-prompt` 的 `personaPrefix` / `personaSuffix` 提供,
64
+ 只在 `@morlay/dsh-preset` 的 `cordis.patch.yml` 维护一份;`keep` 与之一致。
65
+ - 裁剪与降级由同一份 `keep` 名单决定,不存在"系统提示词删了、reminder 没送"的
66
+ 偏差。上游 `persona` 行的 `complete: true` 也能把系统提示词收到只剩 persona,
67
+ 但它同时丢弃 suffix,且子 agent 注册 persona 时 `complete` 标记消失——本插件
68
+ 不做这种依赖顺序的事。
69
+ - 本插件不写会话事件,reminder 走 pre-step 的消息通道(模型可见即可重建,落库由
70
+ loop 负责)。
71
+
72
+ ## 已知限制
73
+
74
+ - **reminder 仍是模型输入**:总 token 不减,只是不再占系统提示词的位置;要真正
75
+ 省 token 得把内容一起删掉。
76
+ - **回退边缘**:rewind 掉旧 reminder 后,若当前文本与内存缓存仍相同,不会立即补
77
+ 一条(重启或下次文本变化时恢复)。
78
+ - **顺序**:reminder 与工作区指令(`AGENTS.md`)都是 pre-step 注入的 user 消息,
79
+ 两者先后由 listener 注册顺序决定,语义上互不依赖。
@@ -0,0 +1,3 @@
1
+ # 本包是纯插件:装配点是 agent preset composition 里的一行(见 README),
2
+ # 不经 bundle patch;空 patch 只为满足包 exports 约定。
3
+ []
@@ -0,0 +1,22 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ //#region src/index.d.ts
4
+ /** Cordis 插件名。 */
5
+ declare const name = "prompt-reminder";
6
+ /** 装配点依赖的系统提示词注册表。 */
7
+ declare const inject: string[];
8
+ /** 插件配置。 */
9
+ interface Config {
10
+ /** 留在系统提示词里的 section 名;其余非空 section 降级为 reminder。 */
11
+ keep?: string[];
12
+ }
13
+ /** 运行时配置 schema。 */
14
+ declare const Config: z<Config>;
15
+ /**
16
+ * 注册裁剪与注入。
17
+ * @param ctx - preset 的 standing scope 上下文(覆盖该 preset 的全部 agent)。
18
+ * @param config - 保留名单。
19
+ */
20
+ declare function apply(ctx: Context, config: Config): void;
21
+ //#endregion
22
+ export { Config, apply, inject, name };
package/dist/index.mjs ADDED
@@ -0,0 +1,101 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION } from "@deepseek-ai/dsh-system-prompt";
3
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
4
+ //#region src/reminder.ts
5
+ const REMINDER_OPEN = "<system-reminder>";
6
+ const REMINDER_CLOSE = "</system-reminder>";
7
+ const REMINDER_INTRO = "The system prompt for this session is deliberately minimal. The guidance below belongs to it and stays in force; the newest reminder supersedes every earlier one.";
8
+ function escapeFrameBody(body) {
9
+ return body.replaceAll(REMINDER_CLOSE, "<\\/system-reminder>");
10
+ }
11
+ /** 按原 order 拼接降级 section 文本,渲染一条 reminder 消息文本。 */
12
+ function renderReminder(sectionTexts) {
13
+ const body = sectionTexts.map(escapeFrameBody).join("\n\n");
14
+ return `${REMINDER_OPEN}\n${REMINDER_INTRO}\n\n${body}\n${REMINDER_CLOSE}`;
15
+ }
16
+ /** 该 user 消息是否由本插件注入。 */
17
+ function isPromptReminder(message) {
18
+ return message.source.kind === "prompt-reminder";
19
+ }
20
+ /** 会话 surface 上最近一条 reminder 的文本;跨进程重启后据此恢复幂等。 */
21
+ function latestReminderText(agent) {
22
+ for (const seq of agent.session.surface.nodes.toReversed()) {
23
+ const event = agent.session.eventAt(seq);
24
+ if (event?.type !== "user/message" || !isPromptReminder(event.data)) continue;
25
+ const [block] = event.data.content;
26
+ return event.data.content.length === 1 && block?.type === "text" ? block.text : "";
27
+ }
28
+ }
29
+ /** 构造一条 reminder user 消息。 */
30
+ function reminderMessage(text) {
31
+ return createUserMessage({
32
+ content: [{
33
+ type: "text",
34
+ text
35
+ }],
36
+ source: {
37
+ kind: "prompt-reminder",
38
+ form: "instructions"
39
+ }
40
+ });
41
+ }
42
+ //#endregion
43
+ //#region src/index.ts
44
+ /** Cordis 插件名。 */
45
+ const name = "prompt-reminder";
46
+ /** 装配点依赖的系统提示词注册表。 */
47
+ const inject = ["systemPrompt"];
48
+ /** 运行时配置 schema。 */
49
+ const Config = z.object({ keep: z.array(z.string()).default([PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION]) });
50
+ /** 拆出要保留的 section 与要降级的文本;空文本 section 不降级(渲染时本就丢弃)。 */
51
+ function demote(sections, keep) {
52
+ const kept = [];
53
+ const texts = [];
54
+ for (const section of sections) if (keep.has(section.name) || section.text.length === 0) kept.push(section);
55
+ else texts.push(section.text);
56
+ return {
57
+ sections: kept,
58
+ texts
59
+ };
60
+ }
61
+ /**
62
+ * 注册裁剪与注入。
63
+ * @param ctx - preset 的 standing scope 上下文(覆盖该 preset 的全部 agent)。
64
+ * @param config - 保留名单。
65
+ */
66
+ function apply(ctx, config) {
67
+ const keep = new Set(config.keep ?? []);
68
+ const captured = /* @__PURE__ */ new WeakMap();
69
+ const delivered = /* @__PURE__ */ new WeakMap();
70
+ ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
71
+ const result = await next();
72
+ const { sections, texts } = demote(result.sections, keep);
73
+ const agent = context.agent;
74
+ if (agent !== void 0) {
75
+ if (texts.length === 0) captured.delete(agent);
76
+ else captured.set(agent, renderReminder(texts));
77
+ }
78
+ return texts.length === 0 ? result : {
79
+ ...result,
80
+ sections
81
+ };
82
+ });
83
+ ctx.on("agent/pre-step", async ({ agent, messages }, next) => {
84
+ const decision = await next();
85
+ if (decision.kind === "reject" || decision.messages.length === 0) return decision;
86
+ const reminder = captured.get(agent);
87
+ if (reminder === void 0 || delivered.get(agent) === reminder) return decision;
88
+ if (latestReminderText(agent) === reminder) {
89
+ delivered.set(agent, reminder);
90
+ return decision;
91
+ }
92
+ delivered.set(agent, reminder);
93
+ const claimedEnd = decision.messages.findLastIndex((message) => messages.includes(message));
94
+ return {
95
+ ...decision,
96
+ messages: decision.messages.toSpliced(claimedEnd + 1, 0, reminderMessage(reminder))
97
+ };
98
+ });
99
+ }
100
+ //#endregion
101
+ export { Config, apply, inject, name };
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@morlay/dsh-prompt-reminder",
3
+ "version": "0.0.1",
4
+ "description": "Demotes the system-prompt sections a reduced prompt leaves out into <system-reminder> user messages, so tool guidance stops diluting the system prompt while it stays in force.",
5
+ "keywords": [
6
+ "dsh",
7
+ "dsh-plugin",
8
+ "preset",
9
+ "reminder",
10
+ "system-prompt"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/morlay/better-session.git"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "cordis.patch.yml",
21
+ "!**/__tests__"
22
+ ],
23
+ "type": "module",
24
+ "exports": {
25
+ ".": "./dist/index.mjs",
26
+ "./package.json": "./package.json",
27
+ "./cordis.patch.yml": "./cordis.patch.yml"
28
+ },
29
+ "dependencies": {
30
+ "@deepseek-ai/schemastery": "^3.18.2"
31
+ },
32
+ "devDependencies": {
33
+ "@deepseek-ai/cordis": "^4.0.2",
34
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
35
+ "@deepseek-ai/dsh-agent-loop": "^0.1.5-rc.2",
36
+ "@deepseek-ai/dsh-agent-loop-testkit": "^0.1.5-rc.2",
37
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
38
+ "@deepseek-ai/dsh-scope": "^0.1.5-rc.2",
39
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
40
+ "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2"
41
+ },
42
+ "peerDependencies": {
43
+ "@deepseek-ai/cordis": "^4.0.2",
44
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
45
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
46
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
47
+ "@deepseek-ai/dsh-system-prompt": "^0.1.5-rc.2"
48
+ },
49
+ "scripts": {
50
+ "build": "pnpm exec tsdown"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,92 @@
1
+ /**
2
+ * 把精简系统提示词里被裁掉的 section 降级为 `<system-reminder>` user 消息:
3
+ * 装配时从 assembly 移除并捕获,pre-step 时注入,内容变化才追加新 reminder。
4
+ *
5
+ * 与 preset 的 `persona` 行配合使用:`keep` 名单之外的 section 不再进入系统
6
+ * 提示词,而是紧随本轮用户消息之后送达,避免工具说明稀释系统提示词。
7
+ * @module @morlay/dsh-prompt-reminder
8
+ */
9
+
10
+ import type { Context } from "@deepseek-ai/cordis";
11
+ import z from "@deepseek-ai/schemastery";
12
+ import type { Agent } from "@deepseek-ai/dsh-agent";
13
+ import {
14
+ PERSONA_PREFIX_SECTION,
15
+ PERSONA_SUFFIX_SECTION,
16
+ type AssembledSection,
17
+ } from "@deepseek-ai/dsh-system-prompt";
18
+ import { latestReminderText, reminderMessage, renderReminder } from "./reminder.ts";
19
+
20
+ /** Cordis 插件名。 */
21
+ export const name = "prompt-reminder";
22
+
23
+ /** 装配点依赖的系统提示词注册表。 */
24
+ export const inject = ["systemPrompt"];
25
+
26
+ /** 插件配置。 */
27
+ export interface Config {
28
+ /** 留在系统提示词里的 section 名;其余非空 section 降级为 reminder。 */
29
+ keep?: string[];
30
+ }
31
+
32
+ /** 运行时配置 schema。 */
33
+ export const Config: z<Config> = z.object({
34
+ keep: z.array(z.string()).default([PERSONA_PREFIX_SECTION, PERSONA_SUFFIX_SECTION]),
35
+ });
36
+
37
+ /** 拆出要保留的 section 与要降级的文本;空文本 section 不降级(渲染时本就丢弃)。 */
38
+ function demote(
39
+ sections: readonly AssembledSection[],
40
+ keep: ReadonlySet<string>,
41
+ ): { sections: AssembledSection[]; texts: string[] } {
42
+ const kept: AssembledSection[] = [];
43
+ const texts: string[] = [];
44
+ for (const section of sections) {
45
+ if (keep.has(section.name) || section.text.length === 0) kept.push(section);
46
+ else texts.push(section.text);
47
+ }
48
+ return { sections: kept, texts };
49
+ }
50
+
51
+ /**
52
+ * 注册裁剪与注入。
53
+ * @param ctx - preset 的 standing scope 上下文(覆盖该 preset 的全部 agent)。
54
+ * @param config - 保留名单。
55
+ */
56
+ export function apply(ctx: Context, config: Config): void {
57
+ const keep = new Set(config.keep ?? []);
58
+ // 本轮装配捕获的 reminder 文本,按 agent 区分(子 agent 各有自己的组合与 persona)。
59
+ const captured = new WeakMap<Agent, string>();
60
+ // 已知送达 surface 的文本,稳态下据此跳过 surface 扫描。
61
+ const delivered = new WeakMap<Agent, string>();
62
+
63
+ ctx.on("system-prompt/assemble", async (_assembly, context, next) => {
64
+ const result = await next();
65
+ const { sections, texts } = demote(result.sections, keep);
66
+ const agent = context.agent;
67
+ if (agent !== undefined) {
68
+ if (texts.length === 0) captured.delete(agent);
69
+ else captured.set(agent, renderReminder(texts));
70
+ }
71
+ return texts.length === 0 ? result : { ...result, sections };
72
+ });
73
+
74
+ ctx.on("agent/pre-step", async ({ agent, messages }, next) => {
75
+ const decision = await next();
76
+ // 没有进入模型的消息时不注入:空消息列表属于 no-step turn,下一轮再试。
77
+ if (decision.kind === "reject" || decision.messages.length === 0) return decision;
78
+ const reminder = captured.get(agent);
79
+ if (reminder === undefined || delivered.get(agent) === reminder) return decision;
80
+ if (latestReminderText(agent) === reminder) {
81
+ delivered.set(agent, reminder);
82
+ return decision;
83
+ }
84
+ delivered.set(agent, reminder);
85
+ // 贴近已领取的这批消息之后:直接输入在前,注入的上下文紧随其后。
86
+ const claimedEnd = decision.messages.findLastIndex((message) => messages.includes(message));
87
+ return {
88
+ ...decision,
89
+ messages: decision.messages.toSpliced(claimedEnd + 1, 0, reminderMessage(reminder)),
90
+ };
91
+ });
92
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * 渲染与识别降级 reminder:信封沿用工作区指令的 `<system-reminder>` 约定。
3
+ * @module @morlay/dsh-prompt-reminder/reminder
4
+ */
5
+
6
+ import type { Agent } from "@deepseek-ai/dsh-agent";
7
+ import type {} from "@deepseek-ai/dsh-llm";
8
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
9
+ import type { UserMessage } from "@deepseek-ai/dsh-session";
10
+
11
+ const REMINDER_OPEN = "<system-reminder>";
12
+ const REMINDER_CLOSE = "</system-reminder>";
13
+
14
+ // 首行声明来源与替代语义:模型需要知道这些是系统级规则、且最新一条覆盖旧的。
15
+ const REMINDER_INTRO =
16
+ "The system prompt for this session is deliberately minimal. The guidance below belongs to it and stays in force; the newest reminder supersedes every earlier one.";
17
+
18
+ /** 本插件注入的 user 消息来源(`MessageSourceMap` 是 merge-extensible 的)。 */
19
+ export interface PromptReminderSource {
20
+ kind: "prompt-reminder";
21
+ form: "instructions";
22
+ }
23
+
24
+ declare module "@deepseek-ai/dsh-llm" {
25
+ interface MessageSourceMap {
26
+ "prompt-reminder": PromptReminderSource;
27
+ }
28
+ }
29
+
30
+ // 正文里出现闭合标记会提前结束信封,替换成转义写法。
31
+ function escapeFrameBody(body: string): string {
32
+ return body.replaceAll(REMINDER_CLOSE, "<\\/system-reminder>");
33
+ }
34
+
35
+ /** 按原 order 拼接降级 section 文本,渲染一条 reminder 消息文本。 */
36
+ export function renderReminder(sectionTexts: readonly string[]): string {
37
+ const body = sectionTexts.map(escapeFrameBody).join("\n\n");
38
+ return `${REMINDER_OPEN}\n${REMINDER_INTRO}\n\n${body}\n${REMINDER_CLOSE}`;
39
+ }
40
+
41
+ /** 该 user 消息是否由本插件注入。 */
42
+ export function isPromptReminder(message: UserMessage): boolean {
43
+ return message.source.kind === "prompt-reminder";
44
+ }
45
+
46
+ /** 会话 surface 上最近一条 reminder 的文本;跨进程重启后据此恢复幂等。 */
47
+ export function latestReminderText(agent: Agent): string | undefined {
48
+ for (const seq of agent.session.surface.nodes.toReversed()) {
49
+ const event = agent.session.eventAt(seq);
50
+ if (event?.type !== "user/message" || !isPromptReminder(event.data)) continue;
51
+ const [block] = event.data.content;
52
+ return event.data.content.length === 1 && block?.type === "text" ? block.text : "";
53
+ }
54
+ return undefined;
55
+ }
56
+
57
+ /** 构造一条 reminder user 消息。 */
58
+ export function reminderMessage(text: string): UserMessage {
59
+ return createUserMessage({
60
+ content: [{ type: "text", text }],
61
+ source: { kind: "prompt-reminder", form: "instructions" },
62
+ });
63
+ }