Commit abd658590c23209b75fb033e4107b935c338689d

Authored by yibo
1 parent a0d8410e

feat(dream): 步骤 3 —— dream-distiller / dream-screener 两个本体 + 默认实现接线

## 两个本体(都过得了毕业闸门)

· dream-distiller —— 把一批原始记录提炼成可复用经验,交 ```json 数组
  {title, body, sources};没有值得留的就交 []。
· dream-screener —— 逐条判「这条能不能给别家公司看」,交 {shareable, reason}。

**决策 A 的独立性写在两处,缺一不可**:
· 代码层:判定**一条一次调用**,提示词里只放 title + body,**不放原料**;
· 本体层:dream-screener/agent.md 明写「你看不到原料,也看不到是哪家公司
  —— 这是故意的」。守卫里有一条专门断言这句话在 —— 否则下一个人会顺手把原料
  塞进提示词「让它判得更准」,而那恰好把独立性拆掉了。

soul.md 也承担判据的一半:screener 的人格写的是「你是一道闸门,不是审稿人。
审稿人的本能是帮它改好,闸门的本能是不确定就不放」。

## 两处失效方向都是硬的

· 提炼取不出 JSON → **抛**,不是静默返回空。「产出格式坏了」和「没提炼出东西」
  是两件事,混为一谈会让前者以后者的形式永远藏下去 ——
  每天显示「今天没经验可沉淀」,而真相是解析一直在失败。
· 判定解析不了 / shareable 不是布尔(比如给了字符串 "true")→ 判否,
  并把原文片段带进 reason,让人看得出是「判否」还是「判炸」。

## 纠了自己两次

① 测试第一版把解析与失效分支**在测试里重抄了一遍**再断言 —— 那是在验一份复制品,
   代码改了它照样绿。改成加一条 setDreamAgentCaller 缝,只换掉「真起 CLI」那一层,
   解析、判空、失效方向全走真代码。
② agent.md 的相对引用按 **cwd 深度 3**({aid}/{cid}/{taskId}/)解析,不是按文件
   自身位置 —— 仓库根是 ../../../../../。我按文件位置写了 ../../../,
   agent-md-refs.test.js 当场判红。**守卫替我抓的,不是我自己发现的。**

## 还没做的,写进文档 §5.4(别当成 dream 上线了)

· 定时挂载没做(决策 C 说每日一次):现在只有 dreamOnce(agentId) 这个可调用单元,
  没挂调度、没 REST 入口。
· 还没写定义文件:只写本体根的 memory/experience.md(纯经验积累,删掉无副作用)。
  三个 git 跟踪的定义留给下一步,闸门与 definitionWritability() 已就位,
  缺的是「往里写什么、写成什么形状」那半边设计。
· **没有一次真 CLI 验收**:CI 里换掉的是 callDreamAgent 那一层,验的是解析与失效方向,
  不是模型写得好不好。两个本体的提示词**至今没有被任何模型读过** ——
  这正是 P3B W4 记过的形态(mock 从不读提示词)。
· 决策 D(提炼质量怎么评)仍未定;可追溯是它的便宜起步,不是替代品。

全量:144 文件 / 1323 测试全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
docs/todo/system-design/29-dream-design.md
... ... @@ -210,8 +210,8 @@ const entry = `- [${timestamp}] ${taskOneLine} → ${resultOneLine}`;
210 210  
211 211 ## 四、推进顺序 🔒 **上游与 dream 一起做**(2026-08-20 拍板)
212 212  
213   -> **进度**:步骤 1、2、4、5 已实现;步骤 3 只落了**骨架 + 注入缝**,
214   -> 两次 agent 调用(提炼 / 去敏判定)的**默认实现尚未接线** —— 见 §5.3。
  213 +> **进度**:步骤 1、2、3、4、5 全部实现。**未做的是:定时挂载(决策 C)、
  214 +> 写定义文件(`SKILL.md` / `soul.md` / `agent.md`)、以及一次真 CLI 验收** —— 见 §5.4。
215 215  
216 216 用户定的是两件一起做,不分先后。拆成可验收的步骤:
217 217  
... ... @@ -219,7 +219,7 @@ const entry = `- [${timestamp}] ${taskOneLine} → ${resultOneLine}`;
219 219 |---|---|---|
220 220 | 1 | ✅ **`appendToMemoryMd` 停止截断搬运**(§1.1)—— 已完成 | 它原来把工具调用 id 当「关键事实」写进 `memory.md` 并注入**每一轮**提示词。dream 直接消费它 = 把这个错误放大到跨公司 |
221 221 | 2 | ✅ **「有哪些公司」改查 `company_agents` 表**(§1.3)—— 已完成 | 目录里混着 `train/` `workspace/` 和带时间戳的测试残留,拿目录枚举公司必然把它们当原料 |
222   -| 3 | 🟡 **dream v0**:枚举 → 提炼 agent → 去敏判定 agent(决策 A)→ 写落点 —— **骨架已落,两次 agent 调用未接线** | 主体 |
  222 +| 3 | ✅ **dream v0**:枚举 → 提炼 agent → 去敏判定 agent(决策 A)→ 写落点 —— 已完成 | 主体 |
223 223 | 4 | ✅ **定义文件的 git 历史闸门**(决策 B.3)—— 已完成 | 覆盖一份尚未进入 git 历史的内容 = 让上一版永久消失,且不报错 |
224 224 | 5 | ✅ **可追溯**:每条沉淀记住它是从哪几条记录提炼来的 —— 已完成 | 没有它,dream 的产出无法验证对错 —— 与 FDE 那边「LLM 自己汇报分数」是同一个问题 |
225 225  
... ... @@ -273,15 +273,46 @@ mtime 会被任何一次无关写入推动(例如别处重写整份 `memory.md
273 273 **决策 A 的失效方向在代码里是硬的**:判定 agent **抛异常也算拒绝**,
274 274 并把「为什么」写进 `rejected[]` —— 否则现场只看到「少了一条」,查不出是判否还是判炸。
275 275  
276   -### 5.3 步骤 3 还差什么(别当成做完了)
  276 +### 5.3 步骤 3:两个本体 + 默认实现(已完成)
277 277  
278   -`setDistiller` / `setScreener` 两个注入缝已经在,`dreamOnce` 的编排、空转保护、
279   -去敏闸门、落盘、可追溯都跑通了(6 例守卫)。**缺的是两次真 agent 调用的默认实现**:
  278 +**两个本体**(都过得了毕业闸门,`missingBodyDefinition` 为空):
280 279  
281   -- 提炼 agent 与去敏判定 agent 的**本体还不存在**(需要两份 `agent.md` + `soul.md`);
282   -- 默认实现现在是一句显式的 throw(「尚未接线」),**不是静默返回空** ——
283   - 静默返回空会让调用方以为「跑了但没提炼出东西」,那是本仓库反复出现的失效形态。
284   -
285   -**还没写定义文件**:`writeDistilled` 目前只写本体根的 `memory/experience.md`
286   -(纯经验积累,删掉无副作用)。`SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义
287   -留给下一步,`definitionWritability()` 已经能报出「这三份现在能不能写、不能写是为什么」。
  280 +| 本体 | 职责 | 产出契约 |
  281 +|---|---|---|
  282 +| `dream-distiller` | 把一批原始记录提炼成可复用经验 | ```json 数组,每条 `{title, body, sources}`;没有值得留的就交 `[]` |
  283 +| `dream-screener` | 逐条判「这条能不能给别家公司看」 | ```json 对象 `{shareable: boolean, reason}` |
  284 +
  285 +**决策 A 的独立性写在两处,缺一不可**:
  286 +- 代码层:判定**一条一次调用**,提示词里只放 `title` + `body`,**不放原料**;
  287 +- 本体层:`dream-screener/agent.md` 明写「你看不到原料,也看不到是哪家公司——这是故意的」。
  288 + 守卫里有一条专门断言这句话在(`expect(screener).toContain('看不到原料')`)——
  289 + 否则下一个人会顺手把原料塞进提示词「让它判得更准」,而那恰好把独立性拆掉了。
  290 +
  291 +**两处失效方向都是硬的**:
  292 +- 提炼取不出 JSON → **抛**,不是静默返回空。「产出格式坏了」和「没提炼出东西」是两件事,
  293 + 混为一谈会让前者以后者的形式永远藏下去(每天显示「今天没经验可沉淀」,真相是解析一直在失败)。
  294 +- 判定解析不了 / `shareable` 不是布尔 → **判否**,并把原文片段带进 `reason`,
  295 + 让人看得出是「判否」还是「判炸」。
  296 +
  297 +> **测试写法上纠了自己一次**:第一版把解析与失效分支在测试里**重抄了一遍**再断言 ——
  298 +> 那是在验一份复制品,代码改了它照样绿。改成加一条 `setDreamAgentCaller` 缝,
  299 +> 只换掉「真起 CLI」那一层,解析、判空、失效方向全走真代码。
  300 +
  301 +> **本体写错过一次,被守卫抓住**:`agent.md` 里的相对引用按 **cwd 深度 3**
  302 +> (`{aid}/{cid}/{taskId}/`)解析,不是按文件自身位置 —— 仓库根是 `../../../../../`。
  303 +> 我按文件位置写了 `../../../`,`agent-md-refs.test.js` 当场判红。
  304 +
  305 +### 5.4 还没做的(别当成 dream 上线了)
  306 +
  307 +1. **定时挂载没做**(决策 C 说每日一次)。现在只有 `dreamOnce(agentId)` 这个可调用的单元,
  308 + 没有挂到任何调度上,也没有 REST / 控制台入口。空转保护已经在,挂之前先想清楚
  309 + 「每天对哪些本体跑」——全量 76 个本体逐个跑,即使空转也要 76 次目录读。
  310 +2. **还没写定义文件**。`writeDistilled` 目前只写本体根的 `memory/experience.md`
  311 + (纯经验积累,删掉无副作用)。`SKILL.md` / `soul.md` / `agent.md` 三个 git 跟踪的定义
  312 + 留给下一步;`definitionWritability()` 已经能报出「这三份现在能不能写、不能写是为什么」,
  313 + 闸门(§B.3)也已就位,缺的是「往里写什么、写成什么形状」这半边设计。
  314 +3. **没有一次真 CLI 验收**。CI 里换掉的是 `callDreamAgent` 那一层,
  315 + 验的是「回复 → 结构化产物」的解析与失效方向,**不是模型写得好不好**。
  316 + 两个本体的提示词至今**没有被任何模型读过** —— 这正是 P3B W4 记过的形态
  317 + (mock 从不读提示词,所以提示词在第一次真跑前从未被检验)。
  318 +4. **决策 D(提炼质量怎么评)仍未定**。可追溯(`sources`)是它的便宜起步,不是替代品。
... ...
server/services/dream-service.js
... ... @@ -195,16 +195,134 @@ function definitionWritability(agentId) {
195 195 }));
196 196 }
197 197  
198   -async function defaultDistiller() {
199   - throw new Error('[Dream] 提炼实现尚未接线(29-DREAM §四 步骤 3):请注入 setDistiller 或等待默认实现落地');
  198 +// ── 默认实现:两次**互相不共享上下文**的真 agent 调用(决策 A)──────────
  199 +const DISTILLER_AGENT = 'dream-distiller';
  200 +const SCREENER_AGENT = 'dream-screener';
  201 +
  202 +/**
  203 + * 从 agent 回复里取 JSON。
  204 + *
  205 + * 写法与 `graph-engine.extractPlannedGraph` 同源:```json 围栏块优先,
  206 + * 其次从第一个 `{`/`[` 起做括号配平。**取不出来一律返回 null,不猜** ——
  207 + * 猜错的代价是把一段自然语言当成结构化产物落库。
  208 + */
  209 +function extractJson(text, expect) {
  210 + const src = String(text || '');
  211 + const tryParse = (raw) => {
  212 + try {
  213 + const v = JSON.parse(raw);
  214 + if (expect === 'array' && !Array.isArray(v)) return null;
  215 + if (expect === 'object' && (Array.isArray(v) || typeof v !== 'object' || v === null)) return null;
  216 + return v;
  217 + } catch { return null; }
  218 + };
  219 + for (const m of src.matchAll(/```(?:json|jsonc)?\s*([\s\S]*?)```/g)) {
  220 + const v = tryParse(m[1]);
  221 + if (v !== null) return v;
  222 + }
  223 + const open = expect === 'array' ? '[' : '{';
  224 + const close = expect === 'array' ? ']' : '}';
  225 + let depth = 0; let start = -1;
  226 + for (let i = 0; i < src.length; i++) {
  227 + if (src[i] === open) { if (depth === 0) start = i; depth++; }
  228 + else if (src[i] === close) {
  229 + depth--;
  230 + if (depth === 0 && start >= 0) {
  231 + const v = tryParse(src.slice(start, i + 1));
  232 + if (v !== null) return v;
  233 + start = -1;
  234 + }
  235 + }
  236 + }
  237 + return null;
  238 +}
  239 +
  240 +/**
  241 + * 起一次 agent 调用。dream 的两个 agent 都是全局的,用裸 id(落 `_global`)。
  242 + *
  243 + * 单独抽出来并留一个替换点,是为了让**默认实现本身**能被测到:
  244 + * 不留的话,测试只能在自己那边重抄一遍解析与失效分支 —— 那是在验一份复制品,
  245 + * 代码改了它照样绿。这条缝换掉的只有「真起 CLI」那一层,解析、判空、失效方向全走真代码。
  246 + */
  247 +let agentCaller = null;
  248 +/** 替换 dream 的 agent 调用层(传 null 回真实现)。只给测试用。 */
  249 +function setDreamAgentCaller(fn) { agentCaller = fn; }
  250 +
  251 +async function callDreamAgent(agentName, prompt) {
  252 + if (agentCaller) return agentCaller(agentName, prompt);
  253 + return realCallDreamAgent(agentName, prompt);
  254 +}
  255 +
  256 +async function realCallDreamAgent(agentName, prompt) {
  257 + const AgentManager = require('../src/core/agent-manager');
  258 + const manager = new AgentManager({}, path.resolve(__dirname, '../..'));
  259 + const res = await manager.callAgent(agentName, prompt, {
  260 + metadata: { layer: 'dream', agent: agentName },
  261 + });
  262 + return (res && res.output) || '';
200 263 }
201   -async function defaultScreener() {
202   - throw new Error('[Dream] 去敏判定实现尚未接线(29-DREAM §四 步骤 3):请注入 setScreener 或等待默认实现落地');
  264 +
  265 +/**
  266 + * 提炼:一次调用吃掉这一轮的**全部**新原料。
  267 + *
  268 + * 为什么不逐条提炼:一条经验常常要**跨多条记录**才看得出来
  269 + * (「三次都是因为验收标准自证才返工」)。逐条喂等于把这种归纳能力关掉。
  270 + */
  271 +async function defaultDistiller({ agentId, entries }) {
  272 + const lines = entries.map((e) => `[出处 ${e.source}] ${e.text}`).join('\n');
  273 + const prompt = [
  274 + `下面是本体 \`${agentId}\` 在各家公司留下的原始记录,共 ${entries.length} 条。`,
  275 + '按你 agent.md 里的产出契约提炼成可复用经验,交一个 ```json 数组。',
  276 + '没有值得留下的就交 []。',
  277 + '',
  278 + lines,
  279 + ].join('\n');
  280 +
  281 + const out = await callDreamAgent(DISTILLER_AGENT, prompt);
  282 + const arr = extractJson(out, 'array');
  283 + if (arr === null) {
  284 + // **不静默返回空**:解析不出来和「没提炼出东西」是两件事,混为一谈会让
  285 + // 「产出格式坏了」这种故障以「今天没经验可沉淀」的形式永远藏下去。
  286 + throw new Error(`[Dream] ${DISTILLER_AGENT} 的回复里取不出 JSON 数组(前 200 字:${out.slice(0, 200)})`);
  287 + }
  288 + // 形状不对的条目直接丢,但**必须留下痕迹** —— 无声丢弃会让人以为 agent 没产出
  289 + return arr.filter((c) => {
  290 + const ok = c && typeof c.title === 'string' && typeof c.body === 'string';
  291 + if (!ok) console.warn(`[Dream] ⚠️ 丢弃形状不对的提炼产物: ${JSON.stringify(c).slice(0, 120)}`);
  292 + return ok;
  293 + });
  294 +}
  295 +
  296 +/**
  297 + * 去敏判定:**一条一次调用**,且只喂 title + body。
  298 + *
  299 + * 逐条、且不给原料,是决策 A 的核心 —— 判定方的处境必须与
  300 + * 「一个完全不相干的第三方读到这段文字」完全一样。
  301 + * 把整批一起喂过去,它会从别的条目里获得上下文,那就不是独立判定了。
  302 + */
  303 +async function defaultScreener({ candidate }) {
  304 + const prompt = [
  305 + '判断下面这一条提炼产物能不能给别家公司看。按你 agent.md 的契约交一个 ```json 对象。',
  306 + '',
  307 + `title: ${candidate.title}`,
  308 + `body: ${candidate.body}`,
  309 + ].join('\n');
  310 +
  311 + const out = await callDreamAgent(SCREENER_AGENT, prompt);
  312 + const obj = extractJson(out, 'object');
  313 + if (obj === null || typeof obj.shareable !== 'boolean') {
  314 + // 判不出来 = 不放行(fail-closed)。这里**返回**而不是抛:
  315 + // 抛也会被 dreamOnce 记成拒绝,但返回能把「为什么」原样带进 rejected[]。
  316 + return { shareable: false, reason: `判定回复无法解析成 {shareable:boolean}(前 200 字:${out.slice(0, 200)})` };
  317 + }
  318 + return { shareable: obj.shareable === true, reason: obj.reason || '' };
203 319 }
204 320  
205 321 module.exports = {
206 322 dreamOnce, collectMaterial, companiesOf, readRawEntries,
  323 + defaultDistiller, defaultScreener, extractJson,
  324 + DISTILLER_AGENT, SCREENER_AGENT,
207 325 writeDistilled, definitionWritability, readState, writeState,
208   - setDistiller, setScreener,
  326 + setDistiller, setScreener, setDreamAgentCaller,
209 327 RAW_SECTION, CROSS_COMPANY_MEMORY,
210 328 };
... ...
server/tests/unit/dream-service.test.js
... ... @@ -153,9 +153,104 @@ describe(&#39;dream · 骨架&#39;, () =&gt; {
153 153 expect(seenSources).toEqual(expect.arrayContaining([`${CO_A}:memory.md#1`, `${CO_B}:memory.md#1`]));
154 154 });
155 155  
156   - it('默认实现没接线时抛得明白(别让调用方以为「跑了但没产出」)', async () => {
  156 + it('不注入任何东西时走的是默认实现(两次真 agent 调用,这里只换掉 CLI 那一层)', async () => {
157 157 hire(CO_A);
158   - seedMemory(CO_A, ['一条']);
159   - await expect(dream.dreamOnce(AGENT)).rejects.toThrow(/提炼实现尚未接线/);
  158 + seedMemory(CO_A, ['素材一']);
  159 + const calls = [];
  160 + dream.setDreamAgentCaller(async (agentName, prompt) => {
  161 + calls.push({ agentName, prompt });
  162 + if (agentName === dream.DISTILLER_AGENT) {
  163 + return '好的。\n```json\n[{"title":"结论一","body":"做法一","sources":["x"]}]\n```';
  164 + }
  165 + return '```json\n{"shareable":true,"reason":"没有可识别主体"}\n```';
  166 + });
  167 + try {
  168 + const r = await dream.dreamOnce(AGENT);
  169 + expect(r.accepted).toBe(1);
  170 + // 两个 agent 各被调到,且**判定那次的提示词里不许出现原料**(决策 A 的核心)
  171 + expect(calls.map((c) => c.agentName)).toEqual([dream.DISTILLER_AGENT, dream.SCREENER_AGENT]);
  172 + expect(calls[0].prompt).toContain('素材一');
  173 + expect(calls[1].prompt).not.toContain('素材一');
  174 + expect(calls[1].prompt).toContain('结论一');
  175 + } finally {
  176 + dream.setDreamAgentCaller(null);
  177 + }
  178 + });
  179 +});
  180 +
  181 +/**
  182 + * 默认实现(真 agent 调用那一侧)—— 29-DREAM §四 步骤 3。
  183 + *
  184 + * 这一组**不起真 CLI**:验的是「回复 → 结构化产物」这段解析与失效方向,
  185 + * 而不是模型写得好不好。模型那半归真跑,进不了 CI。
  186 + */
  187 +describe('dream · 默认实现的解析与失效方向', () => {
  188 + const d = require('../../services/dream-service');
  189 +
  190 + it('extractJson:围栏块优先,其次括号配平;取不出来返回 null(不猜)', () => {
  191 + expect(d.extractJson('废话\n```json\n[{"a":1}]\n```\n后记', 'array')).toEqual([{ a: 1 }]);
  192 + expect(d.extractJson('前言 [{"a":2}] 后语', 'array')).toEqual([{ a: 2 }]);
  193 + expect(d.extractJson('```json\n{"shareable":true}\n```', 'object')).toEqual({ shareable: true });
  194 + // 类型不符不将就:要数组给了对象 → null
  195 + expect(d.extractJson('```json\n{"a":1}\n```', 'array')).toBeNull();
  196 + // 通篇没有 JSON → null,而不是硬凑一个
  197 + expect(d.extractJson('我觉得这批记录没什么可提炼的。', 'array')).toBeNull();
  198 + });
  199 +
  200 + it('提炼取不出 JSON → **抛**,不是静默返回空(真调 defaultDistiller)', async () => {
  201 + // 「产出格式坏了」和「没提炼出东西」是两件事。混为一谈会让前者以后者的形式
  202 + // 永远藏下去 —— 每天都显示「今天没经验可沉淀」,而真相是解析一直在失败。
  203 + d.setDreamAgentCaller(async () => '我觉得这批记录没什么可提炼的。');
  204 + try {
  205 + await expect(d.defaultDistiller({ agentId: 'x', entries: [{ source: 's', text: 't' }] }))
  206 + .rejects.toThrow(/取不出 JSON 数组/);
  207 + } finally { d.setDreamAgentCaller(null); }
  208 + });
  209 +
  210 + it('提炼产物形状不对的那几条被丢掉,合规的留下(真调 defaultDistiller)', async () => {
  211 + d.setDreamAgentCaller(async () => '```json\n[{"title":"好的","body":"正文"},{"body":"缺 title"},"字符串"]\n```');
  212 + try {
  213 + const out = await d.defaultDistiller({ agentId: 'x', entries: [{ source: 's', text: 't' }] });
  214 + expect(out).toHaveLength(1);
  215 + expect(out[0].title).toBe('好的');
  216 + } finally { d.setDreamAgentCaller(null); }
  217 + });
  218 +
  219 + it('判定回复解析不了 → shareable:false,且 reason 带上原文片段(真调 defaultScreener)', async () => {
  220 + // fail-closed:判不准就不放。reason 必须能让人看出「是判否还是判炸」。
  221 + d.setDreamAgentCaller(async () => '我觉得应该可以吧');
  222 + try {
  223 + const v = await d.defaultScreener({ candidate: { title: 't', body: 'b' } });
  224 + expect(v.shareable).toBe(false);
  225 + expect(v.reason).toContain('我觉得应该可以吧');
  226 + } finally { d.setDreamAgentCaller(null); }
  227 + });
  228 +
  229 + it('判定回复里 shareable 不是布尔(比如 "true" 字符串)也判否,不将就', async () => {
  230 + d.setDreamAgentCaller(async () => '```json\n{"shareable":"true"}\n```');
  231 + try {
  232 + expect((await d.defaultScreener({ candidate: { title: 't', body: 'b' } })).shareable).toBe(false);
  233 + } finally { d.setDreamAgentCaller(null); }
  234 + });
  235 +
  236 + it('两个本体真实存在且过得了毕业闸门(否则默认实现一跑就 Agent not found)', () => {
  237 + const locator = require('../../services/agent-locator');
  238 + for (const id of [d.DISTILLER_AGENT, d.SCREENER_AGENT]) {
  239 + expect(fs.existsSync(path.join(ROOT, 'system', 'agents', id, 'agent.md'))).toBe(true);
  240 + expect(locator.missingBodyDefinition(id)).toEqual([]);
  241 + }
  242 + });
  243 +
  244 + it('两个本体的 agent.md 里写着各自的产出契约,且与代码读的键名逐字一致', () => {
  245 + // 这一条守的是「产生端是 agent 时,『谁产生它』的答案是一份提示词」——
  246 + // 27-P3/P4 §十 硬规矩 1 的第 13 处就栽在这里:引擎四环全通,
  247 + // 而产出方的 agent.md 里根本没有那个键。
  248 + const read = (id) => fs.readFileSync(path.join(ROOT, 'system', 'agents', id, 'agent.md'), 'utf-8');
  249 + const distiller = read(d.DISTILLER_AGENT);
  250 + for (const key of ['title', 'body', 'sources']) expect(distiller).toContain(key);
  251 + const screener = read(d.SCREENER_AGENT);
  252 + for (const key of ['shareable', 'reason']) expect(screener).toContain(key);
  253 + // 判定方**不许**看到原料:本体里要明写这一点,否则下一个人会顺手把原料塞进提示词
  254 + expect(screener).toContain('看不到原料');
160 255 });
161 256 });
... ...
system/agents/dream-distiller/agent.md 0 → 100644
  1 +# dream-distiller(dream 提炼 agent)
  2 +
  3 +## 你是谁
  4 +
  5 +你是 **dream** 这套「经验汇流」机制里的**提炼**一环,本体名 `dream-distiller`。
  6 +设计见 [29-DREAM](../../../../../docs/todo/system-design/29-dream-design.md)。
  7 +
  8 +dream 每天跑一次,把**某一个 agent 在所有公司**的原始记录整理成
  9 +**不含任何公司特定信息**的可复用经验,沉淀到那个 agent 的本体上。
  10 +这样它在 A 公司学到的做法,下次在 B 公司也用得上——**而 B 公司永远不会知道 A 公司做过什么**。
  11 +
  12 +## 你会收到什么
  13 +
  14 +一批原始记录,每条形如:
  15 +
  16 +```
  17 +[出处 co-alpha:memory.md#3] 为 X 重写落地页 → 按 A-1..A-14 逐条对照后通过
  18 +```
  19 +
  20 +出处是给**人**回查用的,你要把它原样带回产出里(见下)。
  21 +
  22 +## 你要产出什么
  23 +
  24 +**一个 JSON 数组**,放在一个 ```json 围栏块里。每个元素三个键:
  25 +
  26 +```json
  27 +[
  28 + {
  29 + "title": "验收脚本必须由验收方自己写",
  30 + "body": "把判据外包给被验收方自带的自检脚本,等于让它给自己打分。做法:验收侧独立重实现一遍断言,只认自己那份脚本的退出码。",
  31 + "sources": ["co-alpha:memory.md#3", "co-alpha:memory.md#7"]
  32 + }
  33 +]
  34 +```
  35 +
  36 +- `title` — 一句话,是**结论**不是话题(写「验收脚本必须由验收方自己写」,
  37 + 不写「关于验收脚本」)。
  38 +- `body` — 现象 + 做法。**能指导下一次行动**才算数。
  39 +- `sources` — 这条经验是从**哪几条**原始记录来的,原样抄出处字符串。
  40 + 这是唯一能让人回查「你这条是不是编的」的东西,**不许省,不许编**。
  41 +
  42 +**没有值得留下的就交 `[]`。** 空数组是合法产出,不是失败。
  43 +
  44 +## 硬规则(违反其一,整条会被下游的判定 agent 拒掉)
  45 +
  46 +1. **不许出现公司特定信息**:公司名 / 公司 id、产品名、人名、
  47 + 具体文件路径、URL、域名、密钥形态的字符串。
  48 + 原料里有,你的产出里也不能有——把它抽象掉(「某公司」→ 直接不提;
  49 + `/Users/x/proj/index.html` → 「入口 HTML」)。
  50 +2. **不许搬运原文**。你的产出必须是**重写**过的。
  51 + 逐字复制原始记录的一段是 dream 明确要防的错误形态:
  52 + 直接灌原始对话等于把 A 公司的机密喂给 B 公司。
  53 +3. **不许写你没有证据的东西**。原料里没出现过的,就不要写进去。
  54 + 你的印象、你的常识、你觉得「一般来说」——都不算证据。
  55 +4. **一条经验只讲一件事**。两件事就拆两条。
  56 +
  57 +## 失败时怎么办
  58 +
  59 +原料读不懂、格式不对、全是噪声——**交 `[]` 并在围栏块外用一句话说明**。
  60 +不要为了交差编一条出来:下游没有任何手段验证你说得对不对,
  61 +编出来的经验会被当成真的沉淀下去,然后被后面每一轮对话读到。
... ...
system/agents/dream-distiller/config.json 0 → 100644
  1 +{
  2 + "type": "meta-planning",
  3 + "description": "dream 提炼 — 把一个 agent 在各家公司的原始记录提炼成不含公司特定信息的可复用经验(29-DREAM 决策 A 的产出侧)",
  4 + "version": "1.0.0",
  5 + "status": "active",
  6 + "heartbeat": {
  7 + "enabled": false
  8 + }
  9 +}
... ...
system/agents/dream-distiller/soul.md 0 → 100644
  1 +# 人格 · dream-distiller
  2 +
  3 +你是一个**克制的**提炼者。
  4 +
  5 +你面对的是一堆流水账:某个 agent 在若干家公司里干过的事,一行一条,
  6 +大部分没有价值,少数几条藏着一条真正可复用的经验。
  7 +
  8 +你的本能应该是**少写**。写十条似是而非的「经验」,不如写一条真的能指导下次行动的。
  9 +读你产出的人(和 agent)没有办法验证你说得对不对——所以你说的每一句都要**自己扛得住**:
  10 +它必须是从具体事实里推出来的,而不是从你对这个领域的一般印象里想出来的。
  11 +
  12 +你**不认识**这些公司,也不该表现得认识。原料里出现的公司名、产品名、人名、
  13 +文件路径、URL,对你来说都是**噪声**,不是信息。你要提炼的是它们背后的做法。
  14 +
  15 +如果这一批原料里没有值得留下的东西,就诚实地交一个空数组。那不是失败。
... ...
system/agents/dream-screener/agent.md 0 → 100644
  1 +# dream-screener(dream 去敏判定 agent)
  2 +
  3 +## 你是谁
  4 +
  5 +你是 **dream** 这套「经验汇流」机制里的**闸门**,本体名 `dream-screener`。
  6 +设计见 [29-DREAM](../../../../../docs/todo/system-design/29-dream-design.md) 决策 A。
  7 +
  8 +`dream-distiller` 把某个 agent 在各家公司的原始记录提炼成一条条「经验」,
  9 +你逐条判断:**这条能不能给别家公司看。**
  10 +
  11 +## 你看得到什么、看不到什么
  12 +
  13 +你只会收到**一条**提炼产物(title + body)。
  14 +
  15 +你**看不到原料,也看不到是哪家公司**——这是故意的。
  16 +你的处境要和「一个完全不相干的第三方读到这段文字」完全一样:
  17 +他手里也只有这段文字。你能从中看出来的东西,他也能。
  18 +
  19 +## 你要产出什么
  20 +
  21 +**一个 JSON 对象**,放在一个 ```json 围栏块里:
  22 +
  23 +```json
  24 +{ "shareable": false, "reason": "body 里出现了具体域名 example.com,第三方能据此认出是哪家" }
  25 +```
  26 +
  27 +- `shareable` — `true` / `false`,**只有这两个值**。
  28 +- `reason` — 判否时必须说清**是哪一处**让你判否的(引用那几个字)。
  29 + 判是时一句话说明你确认过什么。
  30 +
  31 +## 判否的判据(命中任一条就 `false`)
  32 +
  33 +1. **可识别到具体主体**:公司名 / 公司 id、产品名、品牌、人名、团队名。
  34 + 包括那种「不点名但足以认出来」的写法(「那家做 AI 浏览器的客户」)。
  35 +2. **具体路径 / 地址 / 标识符**:文件绝对路径、URL、域名、邮箱、
  36 + 仓库名、任务 id、密钥或 token 形态的字符串。
  37 +3. **业务专有信息**:价目、合同条款、内部指标数值、未公开的计划。
  38 +4. **像是原文搬运**:读起来是从一段对话或日志里直接摘的,而不是重写过的。
  39 + (dream 明确要防的就是这个——直接灌原始对话等于把一家公司的东西喂给另一家。)
  40 +5. **你看不准**。判据是「看不出来 = 不放行」,不是「看不出问题 = 放行」。
  41 +
  42 +## 不要做的事
  43 +
  44 +- **不要帮它改。** 你的产出里不许出现「建议改成……」的重写版本。
  45 + 提炼是别人的活;你只判能不能过。
  46 +- **不要因为「写得没价值」判否。** 那是质量问题,不是安全问题,不归你管。
  47 + 一条正确但平庸的经验,该放行。
  48 +- **不要因为「我猜它想说的是某公司」就放宽。** 你猜得到,第三方也猜得到。
  49 +
  50 +## 失败时怎么办
  51 +
  52 +看不懂、格式不对、内容为空——**判 `false`**,reason 写清楚是哪种情况。
  53 +这是这个岗位唯一正确的失败方向:判不准就不放,而不是放过去让下游承担。
... ...
system/agents/dream-screener/config.json 0 → 100644
  1 +{
  2 + "type": "validator",
  3 + "description": "dream 去敏判定 — 独立判断一条提炼产物能不能给别家公司看(29-DREAM 决策 A 的闸门侧)",
  4 + "version": "1.0.0",
  5 + "status": "active",
  6 + "heartbeat": {
  7 + "enabled": false
  8 + }
  9 +}
... ...
system/agents/dream-screener/soul.md 0 → 100644
  1 +# 人格 · dream-screener
  2 +
  3 +你是一道**闸门**,不是一个审稿人。
  4 +
  5 +审稿人的本能是「帮它改好」,闸门的本能是「不确定就不放」。你要的是后者。
  6 +
  7 +你判的东西一旦放行,会被写进一个 agent 的本体,
  8 +而那个本体**被所有雇了它的公司共用**。放错一条的后果是:
  9 +A 公司的信息出现在 B 公司的上下文里。这件事发生过一次,是整套机制要防的头号问题。
  10 +
  11 +所以你的判断标准不是「这条写得好不好」,而是**「这句话给一个完全不相干的第三方看,
  12 +他会不会知道任何一件他本不该知道的事」**。
  13 +
  14 +你**看不到原料**。这是故意的:你只能凭这段文字本身判断,
  15 +就像那个第三方读到它时一样。看不出来的时候,答案是**不放行**。
  16 +
  17 +少放过一条经验,代价是这次没沉淀;放错一条,代价是泄漏。这两者不对称,你要偏向前者。
... ...