deer-flow/backend/docs/MEMORY_COGNITIVE_STYLE.md
tomyan007 45411cee69
feat(memory): add cognitiveStyle for collaboration preferences (#3182)
* feat(memory): add cognitiveStyle for collaboration preferences

Extend cross-session memory with user.cognitiveStyle and cognitive facts,
backward-compatible normalization, prompt injection as Thinking Style, and
settings UI. Add design notes and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(memory): normalize legacy memory imports on frontend

Add normalizeMemoryPayload aligned with backend normalize_memory_data,
use it for Settings import and API reads, and add unit/regression tests.
Document schema-evolution steps and sync FACT_EXTRACTION_PROMPT cognitive category.

* fix(memory): harden legacy memory normalization

* fix(memory): keep import envelope validation strict

* fix(memory): keep unknown fields through frontend import normalization

normalizeMemoryPayload rebuilt the memory document from a section whitelist,
so every key it did not name was dropped. Because readMemoryResponse routes
all memory API responses through it, the loss showed up on read, export, and
import alike: revision vanished, and any future backend field would silently
break lossless export/import.

The Gateway models are extra="allow" and deermem normalizes additively, which
left the frontend as the only lossy hop in the chain. Spread before overriding
at each nesting level so normalization only validates and defaults
contract-owned fields, leaving the envelope check as strict as before.

Covered by a unit test on the strict import path, two backend tests for
extension preservation and caller immutability, and an E2E round-trip that
asserts on the captured import request body.

* docs(memory): state the normalization invariant that actually holds

The docstring cited `display` as a backend-driven field alongside
`revision`, but #4546 was closed unmerged and no such field exists in the
contract. It also promised lossless export/import, which the Gateway's
`extra="ignore"` models preclude for any undeclared field.

Replace both with the invariant the code is really defending: the frontend
must not be narrower than the Gateway contract, using the declared
top-level `revision` as the concrete example.

* fix(memory): preserve legacy migration and reject lossy imports

* fix(memory): align legacy fact normalization defaults

* test(memory): use authenticated app for import regression

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-09-23 18:29:12 +08:00

141 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Memory: Cognitive Style
Design note for contributors. Explains `user.cognitiveStyle` and related memory/prompt touchpoints.
## One-line pitch
**Skills teach the agent how to do tasks; memory’s `cognitiveStyle` teaches the agent how to think and collaborate with this user.**
## Problem
Cross-session memory already stores work context, personal preferences, and facts. In practice, two gaps show up:
1. **Semantic mixing** — “Prefers TypeScript” and “Always wants conclusions first, then details” both land in `personalContext` or `behavior` facts. The model must infer which is *collaboration protocol* vs *project preference*.
2. **Wrong layer for collaboration prefs** — Task skills (`SKILL.md`) are procedural and shared. Stable response/collaboration preferences (structure, depth, correction style) are **user-scoped slow variables**, not one-off task steps.
Without an explicit slot, collaboration style is under-specified in injection and easy to drop under token pressure.
## Approach (not a new subsystem)
Extend the existing memory pipeline:
| Layer | Role |
|-------|------|
| `user.cognitiveStyle.summary` | 2–4 sentence paragraph: reasoning & collaboration habits |
| `facts[]` with `category: cognitive` | Atomic, confidence-ranked supplements |
| `normalize_memory_data()` | Backward-compatible fill for older sections and fact metadata |
| `core/prompts/memory_update.chat.yaml` | LLM sets `cognitiveStyle.shouldUpdate` only when new signals are clear |
| `format_memory_for_injection()` | Injects as `Thinking Style:` under User Context |
**Non-goals (this change):**
- Vector / embedding “personality library”
- Separate debounce or sampling schedule for `cognitiveStyle` only
- Replacing `personalContext` or skills
## Update frequency (read vs write)
| Event | Behavior |
|-------|----------|
| **Read (every turn)** | If `injection_enabled`, current `cognitiveStyle` is loaded into `<memory>` within `max_injection_tokens` |
| **Write (after turn)** | `MemoryMiddleware` queues filtered conversation; **debounce** (`debounce_seconds`, default 30s) batches updates |
| **Write (cognitive field)** | Same LLM pass as other user sections; field changes only when JSON has `cognitiveStyle.shouldUpdate: true` |
So: conversations **trigger** the memory job often; **cognitiveStyle text changes** only when the updater model sees durable new evidence—not every chit-chat turn.
## How this differs from nearby concepts
| Concept | Scope | Lifetime |
|---------|--------|----------|
| **Thread / checkpointer** | This session’s messages & tools | Session |
| **Skill** | How to run a task type | Shared / installable |
| **workContext / topOfMind** | What the user is doing | Cross-session, changes often |
| **personalContext** | Language, interests, tone | Cross-session |
| **cognitiveStyle** | How they reason, structure answers, give feedback | Cross-session, **slow** |
| **fact (`cognitive`)** | One line habit or meta-preference | Cross-session, ranked by confidence |
## Adding a new memory field (schema evolution)
When extending the global summary JSON (`user.*` / `history.*`) or the per-agent Markdown fact schema, keep **read**, **import**, migration, and **API** paths aligned so older exports still work.
| Step | Location |
|------|----------|
| 1. Backend normalize | `deerflow/agents/memory/backends/deermem/deermem/core/storage.py` — add keys to `normalize_memory_data()` / fact normalization; update `create_empty_memory()` |
| 2. Frontend normalize | `frontend/src/core/memory/import-memory.ts` — add section keys and normalize recoverable legacy fact metadata before narrowing to `UserMemory` |
| 3. Types & API models | `frontend/src/core/memory/types.ts`, `backend/app/gateway/routers/memory.py` (`UserContext` / `HistoryContext`) |
| 4. Updater prompt | `core/prompts/memory_update.chat.yaml` and `core/prompts/fact_extraction.yaml`; add injection rendering in `core/prompt.py::format_memory_for_injection()`. Fact categories must also be added to `storage.py::CORE_CATEGORIES` |
| 5. Settings UI & i18n | `memory-settings-page.tsx`, `en-US.ts` / `zh-CN.ts` |
| 6. Tests | Backend: legacy sections and facts in `tests/test_memory_storage.py` / `tests/test_memory_normalize.py` / `tests/test_deermem_self_contained.py`. Frontend: import and API-read behavior in `tests/unit/core/memory/` |
| 7. Import path | Keep the stable export envelope strict (`version`, `lastUpdated`, object `user`/`history`, array `facts`), then normalize additive fields inside that valid envelope |
**Avoid:** normalizing only sections while leaving legacy fact metadata unchecked, or making one unrecoverable fact fail the entire background API read. User-initiated imports remain strict for facts without usable content; API reads drop only those unrecoverable entries.
## Verification
```bash
cd backend
PYTHONPATH=. uv run pytest -q tests/test_memory_storage.py tests/test_memory_prompt_injection.py tests/test_memory_normalize.py tests/test_deermem_self_contained.py
PYTHONPATH=. uv run pytest tests/test_memory_router.py -v
cd ../frontend
pnpm test tests/unit/core/memory
```
Manual:
1. Enable `memory` in `config.yaml`, run `make dev`.
2. In a thread, state a stable collaboration rule (e.g. “先给结论,不要长铺垫”).
3. Wait ≥ `debounce_seconds`, open **Settings → Memory** or `GET /api/memory`.
4. Confirm `user.cognitiveStyle.summary` and/or a `cognitive` fact; start a **new thread** and check behavior.
## Issue
**Title:** `feat(memory): add cognitiveStyle for stable reasoning & collaboration habits`
**Summary:**
- Adds `user.cognitiveStyle` to memory schema with backward-compatible normalization.
- Teaches the memory updater to extract thinking/collaboration habits separately from work/personal context.
- Injects as `Thinking Style:` in system prompt; supports `cognitive` fact category.
- Documents rationale in `backend/docs/MEMORY_COGNITIVE_STYLE.md` and harness memory docs.
**Motivation:** Cross-session memory should distinguish project context from stable collaboration preferences (response structure, correction style, depth). This change extends the existing memory harness only; it does not add a new store.
---
## 中文说明
### 背景
跨会话 memory 已有 `workContext`、`personalContext` 与 `behavior` 类 facts。实践中两类信息容易混在同一字段里:
- 项目/工具偏好(例如常用 TypeScript)
- 协作偏好(例如先给结论、控制篇幅、纠错方式)
后者更新频率低于 `topOfMind`,又不同于一次性会话信息。单独增加 `user.cognitiveStyle` 便于注入时固定展示为 `Thinking Style:`,并与任务级 Skill 区分。
### 实现范围
- 扩展全局 summary JSON 与 per-agent Markdown fact schema,`normalize_memory_data()` 兼容旧 section 和缺少元数据的旧 facts
- `core/prompts/memory_update.chat.yaml` 输出 `cognitiveStyle.shouldUpdate`;可选 `category: cognitive` 的 facts
- 复用现有 MemoryManager → DeerMem → 防抖队列 → Updater → 注入链路,不新增子系统
### 更新频率
| 事件 | 行为 |
|------|------|
| 每轮对话开始 | 在 `max_injection_tokens` 内注入已有 `cognitiveStyle`(读) |
| 每轮对话结束 | 与其它 memory 段相同,可能入队;默认 `debounce_seconds` 合并 |
| 写入 `cognitiveStyle` | 仅当 LLM 返回 `shouldUpdate: true` 时更新段落 |
### 与 Skill 的区别
| 类型 | 内容 |
|------|------|
| Skill | 某类任务的步骤与模板(可共享、可安装) |
| `cognitiveStyle` | 该用户稳定的回复结构、讨论深度、反馈习惯(按用户持久化) |
### 以后新增 memory 字段时
按上文 **Adding a new memory field** 清单同步改后端 `normalize_memory_data()` 与前端 `normalizeMemoryPayload()`;导入走 normalize,不要只对完整新 schema 做严校验。后台 API 读取应丢弃无法恢复的单条 fact,而不是让整个 Memory 页面失败。