Commit abd658590c23209b75fb033e4107b935c338689d
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>
Showing
9 changed files
with
429 additions
and
21 deletions
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('dream · 骨架', () => { |
| 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
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
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 | +少放过一条经验,代价是这次没沉淀;放错一条,代价是泄漏。这两者不对称,你要偏向前者。 | ... | ... |