@deepmaid/maid-anchor 0.0.0-stage → 0.1.0-alpha.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) 2026 kenyonxu
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.
@@ -0,0 +1,3 @@
1
+ # 空 patch 层:本包挂载在 preset 声明树(Q3 拍板,2026-10-28 规格 §2);bundle 层仅承载包身份与面板元数据,toggle 对本包为无操作。
2
+ # 空层以顶层 `[]` 承载(修复轮 2 勘正):comment-only 会 parse 为 null,dsh-app-boot parsePatchList 即 throw,整包进 skippedBundles。
3
+ []
package/lib/index.d.ts ADDED
@@ -0,0 +1,113 @@
1
+ //#region src/index.d.ts
2
+ declare module '@deepseek-ai/dsh-llm' {
3
+ interface MessageSourceMap {
4
+ 'plugin:maid-anchor': {
5
+ kind: 'plugin:maid-anchor';
6
+ };
7
+ }
8
+ }
9
+ /**
10
+ * maid-anchor(M2b T7):表达锚种子插件。种子文件(`seeds/<mode>.md`,M2b 只用 default 册)
11
+ * 以仅含 `%%%` 的行分 N 块(N≥2),每块首行 `[user]`/`[assistant]` 角色头:首块必须 user,
12
+ * 角色其后严格交替;system 身份不进种子(由 preset SOUL 承担)。
13
+ *
14
+ * 事件面:dsh 0.1.2-rc.1 起 `Session.get events()` 已删,经 `snapshotEvents()` 读全量日志
15
+ * (升级第 1 批落地;rc.1 前的双读过渡见升级策略第 0 批执行记录)。
16
+ *
17
+ * 注入形态(2026-09-05 第三次勘误,**关键**):整份种子以**一条** `user/message` durable
18
+ * 事件注入,块文按「主人:…/你:…」引述体拼排。不可用 assistant/message 事件承载种子块——
19
+ * web 客户端 ConversationNodeAssembler 把 assistant/message 按 `data.turn:data.step` 归入
20
+ * trajectory-assistant-step 上下文当 update、start 是同 turn:step 的 step/start:任何
21
+ * 非 agent-loop 产出的 assistant/message(带 turn/step 早于 step/start 会让装配器抛错、
22
+ * 事件流订阅整体崩掉;不带则对话页静默空白)都与 web 对话视图不兼容(实机踩坑两晚);
23
+ * 而注入时机只能早于 step/start(种子必须赶在首个 LLM 请求组装前进日志),「落在
24
+ * step/start 之后」的通道对 preset 层插件不存在(session/event firehose 被 scope 过滤,
25
+ * 2026-09-05 产线实证)。user/message 事件无步骤上下文约束、任意位置合法——maid-memory
26
+ * 召回块 / maid-emotion 投影同款路径,restore 形状校验(role 必须 'user')天然满足。
27
+ *
28
+ * 注入通道(2026-09-13 第四次勘误,**dsh 0.1.5/V3 强制**):种子不再 `session.append` 直写,
29
+ * 改经 pre-step **决议消息**承载(居 `decision.messages` 队首)。根因:V3 把系统提示词纳入
30
+ * 消息历史,`system/message` 是受保护首 surface 头,任何消息抢在头前直落都会让会话
31
+ * 「能写不能恢复」(fromRestore 抛 "system/message requires a protected first surface head";
32
+ * emotion 车道 fixture 实录,宿主 Doris 会话同形态必炸);而 rc.2 的 agent/pre-step 发射点在
33
+ * step/start 落盘**之前**,直写必然抢头。决议消息由 agent-loop 在 step 开启时持久化于头之后
34
+ * (maid-memory 召回块同款通道,日志实证),durable/user 角色/引述体/幂等扫描语义全部不变,
35
+ * 且仍赶在首个 LLM 请求组装前进入消息历史。勘误 3 否决的「step/start 之后」指 firehose 观测
36
+ * 通道,决议通道不依赖观测,不受该结论约束。
37
+ *
38
+ * 幂等:per-session Set 快路径 + 会话日志扫描兜底(append-only 日志里的 anchor 事件
39
+ * 不因 compaction 抹除,重启后扫描仍可辨)。会话若跳过了首个 step/start(重启续聊的
40
+ * 旧会话),不补注入——种子是「会话开场」,进行中的对话不做追溯锚定。
41
+ *
42
+ * 种子文件缺失/格式坏 → warn 一次跳过,会话照常(fail-open)。
43
+ */
44
+ declare const name = "maid-anchor";
45
+ type SeedRole = 'user' | 'assistant';
46
+ interface SeedMessage {
47
+ role: SeedRole;
48
+ text: string;
49
+ }
50
+ type SeedBadReason = 'no-separator' | 'first-not-user' | 'role-break' | 'empty-block' | 'block-tag';
51
+ type SeedParseResult = {
52
+ ok: true;
53
+ messages: SeedMessage[];
54
+ } | {
55
+ ok: false;
56
+ reason: SeedBadReason;
57
+ };
58
+ /**
59
+ * 解析种子文件内容:块由「仅含 %%% 的行」分隔(空白容忍,\r\n 兼容);每块首行角色头
60
+ * `[user]`/`[assistant]`,其余为块文(块缘空白剥除、内部保留)。校验:N≥2(即至少一个分隔)、
61
+ * 首块 user、角色严格交替、无空块、块必须带角色头。
62
+ */
63
+ declare function parseSeedFile(content: string): SeedParseResult;
64
+ /** pre-step 决议只穿透不改动;方法声明保持 bivariance,真 cordis Context 结构兼容 */
65
+ interface AnchorLifecycleContext {
66
+ on(event: 'agent/pre-step', listener: (payload: AnchorPreStepPayload, next: () => Promise<AnchorPreStepDecision>) => Promise<AnchorPreStepDecision>): () => void;
67
+ logger?(name: string): {
68
+ info(message: string): void;
69
+ warn(message: string): void;
70
+ };
71
+ /** preset 组合挂载时由 Include 设为组合目录(cordis-plugin-include 先例);宿主裸挂载时可缺省 */
72
+ readonly baseUrl?: string | undefined;
73
+ }
74
+ /** pre-step 决议鸭子面(agent-loop PreStepDecision 的最小消费面;enter 之外形态原样穿透) */
75
+ interface AnchorPreStepDecision {
76
+ kind?: string;
77
+ messages?: unknown[];
78
+ }
79
+ interface AnchorPreStepPayload {
80
+ agent: {
81
+ id: string;
82
+ /** 真运行时是活 Session(ReactLoopAgent.session):events 是 append-only 全量日志 */
83
+ session?: {
84
+ snapshotEvents?: () => readonly unknown[];
85
+ };
86
+ };
87
+ turn: number;
88
+ step: number;
89
+ messages: unknown[];
90
+ signal: AbortSignal;
91
+ }
92
+ interface MaidAnchorConfig {
93
+ /** mode → 种子文件路径(相对 preset 目录/ctx.baseUrl;绝对路径直达;无 baseUrl 时相对 cwd)。M2b 只消费 default 册 */
94
+ seeds?: Record<string, string>;
95
+ /**
96
+ * 来源排除(T11,控制器裁定):命中来源的会话跳过种子注入。判定照 maid-emotion sessionSource
97
+ * 同款——`cron_` sessionId 前缀(maid-cron engine `cron_${jobId}_${iso}`)。**默认 [] = 现状不排除**;
98
+ * 与 maid-emotion excludeSources(默认 ['cron'])不同:anchor 是 preset 层可选件,排除是显式选择
99
+ * 而非缺省政策。命中即跳过注入——幂等标记不写(排除是配置态决定,每轮重判;换配置即刻生效),
100
+ * warn 一次/会话(anchor 侧非例行静默:配置显式排除了本 preset 挂载的车道,值得留痕)。
101
+ */
102
+ excludeSources?: string[];
103
+ }
104
+ /**
105
+ * 会话来源(供 excludeSources 判定)。dsh SessionHeader 无 source/lane 字段;cron 车道唯一
106
+ * 稳定标识是 maid-cron 的 `cron_` sessionId 前缀(engine.ts `cron_${jobId}_${iso}`)。
107
+ */
108
+ declare function sessionSource(sessionId: string): string | undefined;
109
+ /** 交替块按角色标签拼排:user 块 → 「主人:」,assistant 块 → 「你:」(女仆自称视角,不依赖 maid 名)。 */
110
+ declare function renderSeedText(messages: readonly SeedMessage[]): string;
111
+ declare const apply: (ctx: any, config: MaidAnchorConfig) => (() => void);
112
+ //#endregion
113
+ export { AnchorLifecycleContext, AnchorPreStepDecision, AnchorPreStepPayload, MaidAnchorConfig, SeedBadReason, SeedMessage, SeedParseResult, SeedRole, apply, name, parseSeedFile, renderSeedText, sessionSource };
package/lib/index.js ADDED
@@ -0,0 +1,204 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
5
+ //#region src/index.ts
6
+ /**
7
+ * maid-anchor(M2b T7):表达锚种子插件。种子文件(`seeds/<mode>.md`,M2b 只用 default 册)
8
+ * 以仅含 `%%%` 的行分 N 块(N≥2),每块首行 `[user]`/`[assistant]` 角色头:首块必须 user,
9
+ * 角色其后严格交替;system 身份不进种子(由 preset SOUL 承担)。
10
+ *
11
+ * 事件面:dsh 0.1.2-rc.1 起 `Session.get events()` 已删,经 `snapshotEvents()` 读全量日志
12
+ * (升级第 1 批落地;rc.1 前的双读过渡见升级策略第 0 批执行记录)。
13
+ *
14
+ * 注入形态(2026-09-05 第三次勘误,**关键**):整份种子以**一条** `user/message` durable
15
+ * 事件注入,块文按「主人:…/你:…」引述体拼排。不可用 assistant/message 事件承载种子块——
16
+ * web 客户端 ConversationNodeAssembler 把 assistant/message 按 `data.turn:data.step` 归入
17
+ * trajectory-assistant-step 上下文当 update、start 是同 turn:step 的 step/start:任何
18
+ * 非 agent-loop 产出的 assistant/message(带 turn/step 早于 step/start 会让装配器抛错、
19
+ * 事件流订阅整体崩掉;不带则对话页静默空白)都与 web 对话视图不兼容(实机踩坑两晚);
20
+ * 而注入时机只能早于 step/start(种子必须赶在首个 LLM 请求组装前进日志),「落在
21
+ * step/start 之后」的通道对 preset 层插件不存在(session/event firehose 被 scope 过滤,
22
+ * 2026-09-05 产线实证)。user/message 事件无步骤上下文约束、任意位置合法——maid-memory
23
+ * 召回块 / maid-emotion 投影同款路径,restore 形状校验(role 必须 'user')天然满足。
24
+ *
25
+ * 注入通道(2026-09-13 第四次勘误,**dsh 0.1.5/V3 强制**):种子不再 `session.append` 直写,
26
+ * 改经 pre-step **决议消息**承载(居 `decision.messages` 队首)。根因:V3 把系统提示词纳入
27
+ * 消息历史,`system/message` 是受保护首 surface 头,任何消息抢在头前直落都会让会话
28
+ * 「能写不能恢复」(fromRestore 抛 "system/message requires a protected first surface head";
29
+ * emotion 车道 fixture 实录,宿主 Doris 会话同形态必炸);而 rc.2 的 agent/pre-step 发射点在
30
+ * step/start 落盘**之前**,直写必然抢头。决议消息由 agent-loop 在 step 开启时持久化于头之后
31
+ * (maid-memory 召回块同款通道,日志实证),durable/user 角色/引述体/幂等扫描语义全部不变,
32
+ * 且仍赶在首个 LLM 请求组装前进入消息历史。勘误 3 否决的「step/start 之后」指 firehose 观测
33
+ * 通道,决议通道不依赖观测,不受该结论约束。
34
+ *
35
+ * 幂等:per-session Set 快路径 + 会话日志扫描兜底(append-only 日志里的 anchor 事件
36
+ * 不因 compaction 抹除,重启后扫描仍可辨)。会话若跳过了首个 step/start(重启续聊的
37
+ * 旧会话),不补注入——种子是「会话开场」,进行中的对话不做追溯锚定。
38
+ *
39
+ * 种子文件缺失/格式坏 → warn 一次跳过,会话照常(fail-open)。
40
+ */
41
+ const name = "maid-anchor";
42
+ const ANCHOR_PLUGIN = "maid-anchor";
43
+ const SEPARATOR_LINE = "%%%";
44
+ const TAG_LINE = /^\s*\[(user|assistant)\]\s*$/;
45
+ /**
46
+ * 解析种子文件内容:块由「仅含 %%% 的行」分隔(空白容忍,\r\n 兼容);每块首行角色头
47
+ * `[user]`/`[assistant]`,其余为块文(块缘空白剥除、内部保留)。校验:N≥2(即至少一个分隔)、
48
+ * 首块 user、角色严格交替、无空块、块必须带角色头。
49
+ */
50
+ function parseSeedFile(content) {
51
+ const blocks = [[]];
52
+ let separators = 0;
53
+ for (const line of content.split(/\r?\n/)) if (line.trim() === SEPARATOR_LINE) {
54
+ separators += 1;
55
+ blocks.push([]);
56
+ } else blocks[blocks.length - 1].push(line);
57
+ if (separators === 0) return {
58
+ ok: false,
59
+ reason: "no-separator"
60
+ };
61
+ const messages = [];
62
+ for (const block of blocks) {
63
+ const trimmed = block.join("\n").trim();
64
+ if (trimmed === "") return {
65
+ ok: false,
66
+ reason: "empty-block"
67
+ };
68
+ const [tagLine, ...rest] = trimmed.split("\n");
69
+ const tag = TAG_LINE.exec(tagLine ?? "");
70
+ if (tag === null) return {
71
+ ok: false,
72
+ reason: "block-tag"
73
+ };
74
+ const text = rest.join("\n").trim();
75
+ if (text === "") return {
76
+ ok: false,
77
+ reason: "empty-block"
78
+ };
79
+ messages.push({
80
+ role: tag[1],
81
+ text
82
+ });
83
+ }
84
+ if (messages[0].role !== "user") return {
85
+ ok: false,
86
+ reason: "first-not-user"
87
+ };
88
+ for (let i = 1; i < messages.length; i++) if (messages[i].role === messages[i - 1].role) return {
89
+ ok: false,
90
+ reason: "role-break"
91
+ };
92
+ return {
93
+ ok: true,
94
+ messages
95
+ };
96
+ }
97
+ /**
98
+ * 会话来源(供 excludeSources 判定)。dsh SessionHeader 无 source/lane 字段;cron 车道唯一
99
+ * 稳定标识是 maid-cron 的 `cron_` sessionId 前缀(engine.ts `cron_${jobId}_${iso}`)。
100
+ */
101
+ function sessionSource(sessionId) {
102
+ return sessionId.startsWith("cron_") ? "cron" : void 0;
103
+ }
104
+ function logHasAnchorEvent(events) {
105
+ for (const entry of events ?? []) {
106
+ const event = entry;
107
+ if (event?.type === "user/message") {
108
+ const src = event.data?.source;
109
+ if (src?.plugin === ANCHOR_PLUGIN || src?.kind === `plugin:${ANCHOR_PLUGIN}`) return true;
110
+ } else if (event?.type === "assistant/message") {
111
+ if ((event.data?.message)?.source?.provider === ANCHOR_PLUGIN) return true;
112
+ }
113
+ }
114
+ return false;
115
+ }
116
+ const SEED_PROLOGUE = "(以下是你的对话基调示例,供语感参考,不是当前对话)";
117
+ /** 交替块按角色标签拼排:user 块 → 「主人:」,assistant 块 → 「你:」(女仆自称视角,不依赖 maid 名)。 */
118
+ function renderSeedText(messages) {
119
+ const lines = [SEED_PROLOGUE];
120
+ for (const seed of messages) lines.push(seed.role === "user" ? `主人:${seed.text}` : `你:${seed.text}`);
121
+ return lines.join("\n");
122
+ }
123
+ function resolveSeedPath(baseUrl, seedPath) {
124
+ if (isAbsolute(seedPath)) return seedPath;
125
+ if (typeof baseUrl === "string" && baseUrl.startsWith("file:")) try {
126
+ return fileURLToPath(new URL(seedPath, baseUrl));
127
+ } catch {}
128
+ return resolve(process.cwd(), seedPath);
129
+ }
130
+ const apply = (ctx, config) => {
131
+ const logger = ctx.logger?.(ANCHOR_PLUGIN) ?? console;
132
+ const baseUrl = typeof ctx?.baseUrl === "string" ? ctx.baseUrl : void 0;
133
+ const seedsConfig = config?.seeds;
134
+ const defaultSeed = seedsConfig !== null && typeof seedsConfig === "object" ? seedsConfig.default : void 0;
135
+ const excludeSources = Array.isArray(config?.excludeSources) ? config.excludeSources : [];
136
+ const warned = /* @__PURE__ */ new Set();
137
+ const warnOnce = (key, message) => {
138
+ if (warned.has(key)) return;
139
+ warned.add(key);
140
+ logger.warn(message);
141
+ };
142
+ let cached;
143
+ const loadSeeds = () => {
144
+ if (cached !== void 0) return cached;
145
+ if (typeof defaultSeed !== "string" || defaultSeed === "") {
146
+ warnOnce("no-default", "maid-anchor: config.seeds.default 未配置,跳过种子注入");
147
+ return;
148
+ }
149
+ const seedPath = resolveSeedPath(baseUrl, defaultSeed);
150
+ let content;
151
+ try {
152
+ content = readFileSync(seedPath, "utf8");
153
+ } catch (error) {
154
+ warnOnce(`missing:${seedPath}`, `maid-anchor: 种子文件缺失 '${seedPath}'(${error.message}),跳过注入`);
155
+ return;
156
+ }
157
+ const parsed = parseSeedFile(content);
158
+ if (!parsed.ok) {
159
+ warnOnce(`bad:${seedPath}:${parsed.reason}`, `maid-anchor: 种子文件格式坏 '${seedPath}'(${parsed.reason}),跳过注入`);
160
+ return;
161
+ }
162
+ cached = { messages: parsed.messages };
163
+ return cached;
164
+ };
165
+ const handled = /* @__PURE__ */ new Set();
166
+ const disposePreStep = ctx.on("agent/pre-step", async (payload, next) => {
167
+ const sessionId = payload.agent?.id;
168
+ if (typeof sessionId !== "string" || sessionId === "") return next();
169
+ const source = sessionSource(sessionId);
170
+ if (source !== void 0 && excludeSources.includes(source)) {
171
+ warnOnce(`excluded:${sessionId}`, `maid-anchor: 会话来源 '${source}' 命中 excludeSources,跳过种子注入(${sessionId})`);
172
+ return next();
173
+ }
174
+ if (handled.has(sessionId) || logHasAnchorEvent(payload.agent.session?.snapshotEvents?.())) {
175
+ handled.add(sessionId);
176
+ return next();
177
+ }
178
+ const seeds = loadSeeds();
179
+ if (seeds === void 0) {
180
+ handled.add(sessionId);
181
+ return next();
182
+ }
183
+ const seedMessage = createUserMessage({
184
+ content: [{
185
+ type: "text",
186
+ text: renderSeedText(seeds.messages)
187
+ }],
188
+ source: { kind: `plugin:${ANCHOR_PLUGIN}` }
189
+ });
190
+ const decision = await next();
191
+ if (decision?.kind !== "enter" || !Array.isArray(decision.messages)) return decision;
192
+ handled.add(sessionId);
193
+ return {
194
+ ...decision,
195
+ messages: [seedMessage, ...decision.messages]
196
+ };
197
+ });
198
+ return () => {
199
+ handled.clear();
200
+ disposePreStep();
201
+ };
202
+ };
203
+ //#endregion
204
+ export { apply, name, parseSeedFile, renderSeedText, sessionSource };
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@deepmaid/maid-anchor",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0-alpha.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "lib",
16
+ "cordis.patch.yml"
17
+ ],
18
+ "dsh": {
19
+ "engines": {
20
+ "dsh": ">=0.2.1-rc.2 <0.3"
21
+ },
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ },
25
+ "meta": {
26
+ "title": "Maid Anchor",
27
+ "description": "表达锚种子 seam:preset 层挂载的开场气质种子(单条 user 引述体注入)。关闭=表达锚不注入,女仆开场失锚,首因气质漂移。"
28
+ }
29
+ },
30
+ "dependencies": {
31
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.2"
32
+ },
33
+ "devDependencies": {
34
+ "@deepseek-ai/cordis": "4.0.5",
35
+ "@deepseek-ai/dsh-session": "0.2.1-rc.2",
36
+ "tsdown": "0.22.14",
37
+ "typescript": "6.0.3"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown"
44
+ }
6
45
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.