# Ekko Agent 链式记忆系统规划

本文档规划 Ekko Agent 的长期记忆能力。目标不是把完整聊天历史反复塞进模型上下文，而是把对话沉淀成可追踪、可更新、可检索的结构化记忆。

## 目标

- 支持跨轮、跨会话延续上下文。
- 自动从对话中抽取用户偏好、项目事实、决策、任务和知识条目。
- 用链式结构记录记忆演化，保留来源和覆盖关系。
- 支持领域分类，例如 `生活技能 / 做饭 / 菜谱`。
- 控制 token 成本，只在回复时注入少量相关记忆。
- 后续可接入向量检索或知识图谱，但第一版不强依赖。

## 非目标

- 第一版不做完整知识图谱推理。
- 第一版不要求部署 embedding 模型。
- 第一版不让模型直接修改不可追溯的全局记忆。
- 第一版不把所有历史总结成一个不可审计的大摘要。

## 记忆分层

### 1. 原始消息日志

保存完整用户消息、助手消息、工具调用、工具结果和运行元数据。

用途：

- 审计和回放。
- 重新生成摘要。
- 追溯结构化记忆来源。
- 调试模型错误抽取。

### 2. 链式摘要

每隔一段对话生成一个摘要节点。新摘要只依赖上一个摘要和新增消息，避免每次读取完整历史。

示例：

```txt
summary_001 -> summary_002 -> summary_003
```

摘要应包含：

- 当前目标。
- 已完成工作。
- 未完成事项。
- 关键约束。
- 用户偏好。
- 重要决策。
- 已知问题。

### 3. 结构化记忆

结构化记忆保存可以被程序理解和筛选的事实。

推荐类型：

- `preference`: 用户偏好。
- `fact`: 稳定事实。
- `decision`: 决策。
- `task`: 待办或当前任务。
- `recipe`: 菜谱类知识。
- `skill`: 技能步骤或方法。
- `constraint`: 约束。
- `correction`: 用户纠正。

### 4. 可选检索索引

后续可增加：

- keyword/full-text search。
- embedding 向量索引。
- entity/relationship 图索引。

第一版建议先做结构化字段和关键词检索，等数据量变大后再加向量。

## 核心数据模型

### MemoryMessage

```ts
type MemoryMessage = {
  id: string
  sessionId: string
  parentId?: string
  role: 'system' | 'user' | 'assistant' | 'tool'
  content: string
  metadata?: Record<string, unknown>
  createdAt: string
}
```

### MemorySummary

```ts
type MemorySummary = {
  id: string
  sessionId: string
  parentSummaryId?: string
  fromMessageId: string
  toMessageId: string
  summary: string
  currentGoal?: string
  constraints: string[]
  preferences: string[]
  decisions: string[]
  completedWork: string[]
  pendingWork: string[]
  knownIssues: string[]
  createdAt: string
}
```

### MemoryNode

```ts
type MemoryNode = {
  id: string
  parentId?: string
  supersedesId?: string
  sessionId?: string
  workspaceId?: string
  userId?: string
  scope: 'session' | 'workspace' | 'user' | 'global'
  domain: string
  categoryPath: string[]
  type: 'preference' | 'fact' | 'decision' | 'task' | 'recipe' | 'skill' | 'constraint' | 'correction'
  key?: string
  valueJson?: unknown
  title: string
  content: string
  status: 'active' | 'superseded' | 'expired' | 'deleted'
  confidence: number
  importance: number
  tags: string[]
  entities: string[]
  sourceMessageIds: string[]
  createdAt: string
  updatedAt: string
  expiresAt?: string
}
```

## 存取设计

记忆系统建议拆成三个物理表或集合：

```txt
memory_messages
memory_summaries
memory_nodes
memory_audit_events
```

后续如果引入向量，再增加：

```txt
memory_embeddings
```

### 怎么存

一次 agent turn 建议分两段存。

第一段是同步写入原始消息，发生在模型调用前后：

```txt
用户消息进入
  -> append MemoryMessage(role = user)
  -> 组装上下文
  -> 调模型
  -> append MemoryMessage(role = assistant)
```

第二段是异步整理记忆，发生在回复完成后：

```txt
读取上一条 MemorySummary
读取本轮新增 MemoryMessage
调用 memory extractor
得到 MemoryExtraction
按 operation 写入 MemoryNode
必要时 append MemorySummary
```

写入规则：

- 原始消息只追加，不覆盖。
- 摘要只追加，通过 `parentSummaryId` 串成链。
- 结构化记忆默认 upsert，但旧版本不硬删。
- 用户纠正时新建节点，并把旧节点标记为 `superseded`。
- 临时信息写 `expiresAt`，过期后默认不注入上下文。

### 怎么取

回复前取记忆时，不应该读取全部历史。推荐分四路查询后合并：

```txt
1. recentMessages: 当前 session 最近 3-8 条原始消息
2. latestSummary: 当前 session 最新摘要
3. activeNodes: 当前 scope 下高优先级 active 结构化记忆
4. relevantNodes: 根据当前输入做关键词、分类、标签或向量召回
```

查询优先级：

```txt
session scope
> workspace scope
> user scope
> global scope
```

同类记忆冲突时：

```txt
更新的 > 旧的
高置信 > 低置信
临时有效约束 > 长期偏好
用户明确纠正 > 模型推断
```

### Store 接口

第一版可以先定义接口，不绑定具体数据库。

```ts
type MemoryStore = {
  appendMessage(message: MemoryMessage): Promise<void>
  listRecentMessages(input: {
    sessionId: string
    limit: number
  }): Promise<MemoryMessage[]>

  appendSummary(summary: MemorySummary): Promise<void>
  getLatestSummary(input: {
    sessionId: string
  }): Promise<MemorySummary | undefined>

  upsertNode(node: MemoryNode): Promise<void>
  supersedeNode(input: {
    oldNodeId: string
    newNode: MemoryNode
  }): Promise<void>
  deleteNode(input: {
    nodeId: string
    mode: 'soft' | 'hard'
    reason: string
  }): Promise<void>
  listActiveNodes(input: {
    userId?: string
    sessionId?: string
    workspaceId?: string
    domain?: string
    categoryPath?: string[]
    types?: MemoryNode['type'][]
    limit: number
  }): Promise<MemoryNode[]>
  searchNodes(input: {
    query: string
    userId?: string
    sessionId?: string
    workspaceId?: string
    domain?: string
    tags?: string[]
    limit: number
  }): Promise<MemoryNode[]>
}
```

### 精确检索字段

如果要精确回答“用户是不是不吃香菜”这种问题，不能只靠 `title` 和 `content`。需要把可判断的事实写入 `key` 和 `valueJson`。

示例：

```json
{
  "type": "preference",
  "key": "avoid_ingredient",
  "valueJson": "香菜"
}
```

另一个例子：

```json
{
  "type": "preference",
  "key": "flavor_profile",
  "valueJson": {
    "oil": "low",
    "spicy": "low"
  }
}
```

精确检索优先使用：

```txt
scope + domain + categoryPath + type + key + valueJson + status + expiresAt
```

模糊检索才使用：

```txt
title + content + tags + entities + FTS/embedding
```

### Memory Query 接口

`MemoryStore` 面向数据库操作，`MemoryService` 面向 agent 运行时。运行时不应该拼 SQL，而是传入结构化查询。

```ts
type MemoryQuery = {
  userId?: string
  workspaceId?: string
  sessionId?: string
  scopes?: MemoryNode['scope'][]
  domain?: string
  categoryPathPrefix?: string[]
  types?: MemoryNode['type'][]
  key?: string
  valueJson?: unknown
  tags?: string[]
  entities?: string[]
  queryText?: string
  includeExpired?: boolean
  limit?: number
}
```

返回值不是数据库裸结果，而是已经过滤、排序、去冲突后的结果：

```ts
type MemoryQueryResult = {
  exact: MemoryNode[]
  relevant: MemoryNode[]
  omitted: Array<{
    nodeId: string
    reason: 'expired' | 'superseded' | 'low_confidence' | 'conflict_lost' | 'over_limit'
  }>
}
```

## Agent 和 Memory Service 边界

记忆系统不建议设计成一个永远自主运行的 agent。第一版更适合设计成常驻存储和运行时服务：

```txt
AgentRuntime
  -> MemoryService
      -> MemoryStore(SQLite)
      -> MemoryRetriever
      -> MemoryExtractor
      -> MemoryContextBuilder
```

职责边界：

- 主 Agent 负责理解用户请求、推理、调用业务工具、产出回复。
- Memory Service 负责查库、过滤、排序、冲突消解和上下文压缩。
- Memory Extractor 负责在回复后从新增对话中抽取候选记忆。
- Memory Store 负责 SQLite 持久化。

主 Agent 不应该直接：

- 拼 SQL。
- 遍历完整记忆库。
- 判断过期和覆盖关系。
- 自己解决同类记忆冲突。

主 Agent 可以：

- 接收 runtime 自动注入的 `MemoryContext`。
- 在复杂任务里通过 memory tool 请求更多记忆。
- 提出记忆写入建议，但最终由 Memory Service 校验和落库。
- 当用户明确要求记录某件事时，立即调用 memory tool 提交写入请求。

### 默认自动检索

每次调用主模型前，`AgentRuntime` 自动执行一次轻量检索：

```txt
用户消息
  -> buildDefaultMemoryQuery()
  -> MemoryService.retrieve()
  -> buildMemoryContext()
  -> 注入主模型上下文
```

默认检索内容：

- 当前 session 最近消息。
- 当前 session 最新摘要。
- 当前 session active tasks。
- 当前 workspace active constraints / decisions / facts。
- 用户级高优先级 preferences。

这部分不需要主 Agent 主动调用。

### 按需 Memory Tool

复杂场景给主 Agent 暴露工具，但工具背后仍然走 Memory Service。

建议工具：

```ts
memory_search(input: MemoryQuery): Promise<MemoryQueryResult>
```

```ts
memory_get(input: {
  id?: string
  type?: MemoryNode['type']
  key?: string
  valueJson?: unknown
  scope?: MemoryNode['scope']
  domain?: string
}): Promise<MemoryNode | undefined>
```

```ts
memory_propose_update(input: {
  operation: 'create' | 'update' | 'supersede' | 'expire' | 'delete'
  targetId?: string
  node: Partial<MemoryNode>
  reason: string
}): Promise<{
  accepted: boolean
  nodeId?: string
  reason?: string
}>
```

`memory_propose_update` 使用 propose 命名，是为了避免主 Agent 未经校验直接污染长期记忆。Memory Service 需要检查 scope、来源、置信度和冲突关系后再写入。

```ts
memory_forget(input: {
  id?: string
  scope?: MemoryNode['scope']
  domain?: string
  categoryPathPrefix?: string[]
  type?: MemoryNode['type']
  key?: string
  valueJson?: unknown
  mode?: 'soft' | 'hard'
  reason: string
}): Promise<{
  deletedIds: string[]
  mode: 'soft' | 'hard'
  requiresConfirmation?: boolean
  reason?: string
}>
```

### 明确记忆写入

后台 Memory Extractor 适合低频、批量整理；但用户明确要求“记住”“以后都按这个来”“帮我记录一下”时，不应该等到下一次批处理。主 Agent 应该立即调用 `memory_propose_update`，由 Memory Service 校验后同步或准同步写入。

明确写入示例：

```txt
用户：记住，我以后做饭不要香菜。
```

主 Agent 不直接写数据库，而是调用：

```ts
memory_propose_update({
  operation: 'create',
  node: {
    scope: 'user',
    domain: '生活技能',
    categoryPath: ['生活技能', '做饭', '饮食偏好'],
    type: 'preference',
    key: 'avoid_ingredient',
    valueJson: '香菜',
    title: '用户不吃香菜',
    content: '用户明确要求以后做饭推荐不要香菜。',
    status: 'active',
    confidence: 0.99,
    importance: 0.95,
    tags: ['忌口', '菜谱推荐'],
    entities: ['香菜'],
  },
  reason: '用户明确要求记录长期饮食偏好',
})
```

Memory Service 接收到请求后需要：

- 校验 `scope` 是否允许写入。
- 绑定 `sourceMessageIds`，保证可追溯。
- 检查是否已有同类 active 记忆。
- 如果冲突，执行 `supersede` 或要求更明确的用户确认。
- 写入 SQLite 事务。
- 返回是否 accepted。

写入成功后，主 Agent 可以自然确认：

```txt
记住了，以后推荐做饭相关内容时会避开香菜。
```

不建议立即写入的情况：

- 用户只是临时描述状态，例如“今天有点胃不舒服”。
- 模型自己推断出来的偏好，用户没有明确表达。
- 敏感信息或可能涉及隐私的信息。
- scope 不明确且可能影响长期行为的记忆。

这类内容可以进入后台抽取流程，或者先作为 session-scope 临时记忆。

### 删除和遗忘

记忆系统必须支持删除。用户说“忘掉这个”“不要再记这个”“删除我的做饭偏好”时，主 Agent 应调用 `memory_forget`，由 Memory Service 执行删除策略。

删除分两种：

```txt
soft delete: 把 memory_nodes.status 改成 deleted，默认不再检索和注入，但保留审计记录。
hard delete: 从 SQLite 中物理删除节点，并清理 FTS/embedding 索引。
```

第一版默认使用 soft delete。hard delete 适合用户明确要求清除隐私数据，或者执行数据合规删除。

示例：

```txt
用户：忘掉我不吃香菜这件事。
```

主 Agent 调用：

```ts
memory_forget({
  scope: 'user',
  domain: '生活技能',
  categoryPathPrefix: ['生活技能', '做饭', '饮食偏好'],
  type: 'preference',
  key: 'avoid_ingredient',
  valueJson: '香菜',
  mode: 'soft',
  reason: '用户明确要求忘记该饮食偏好',
})
```

Memory Service 需要：

- 先精确查找候选节点。
- 如果命中多条，返回 `requiresConfirmation = true`，让主 Agent 向用户确认范围。
- soft delete 时设置 `status = deleted`，更新 `updatedAt`。
- hard delete 时删除节点、FTS 记录、embedding 记录。
- 记录删除原因和操作者，方便审计。

删除范围建议：

```txt
删除单条记忆：按 id 删除。
删除某类记忆：按 scope + domain + categoryPath + type 删除。
删除某个 workspace 记忆：按 workspaceId 删除。
删除某个用户长期记忆：按 userId + scope=user 删除。
删除来源消息关联记忆：按 sourceMessageIds 删除。
```

注意：删除结构化记忆不一定等于删除原始消息日志。用户如果要求“彻底删除相关对话”，需要同时清理 `memory_messages`、`memory_summaries` 中的相关内容，或者重新生成受影响摘要。

### 冲突消解

Memory Service 在返回记忆前做冲突消解。

排序规则：

```txt
session > workspace > user > global
未过期 > 已过期
用户明确表达 > 模型推断
临时有效约束 > 长期偏好
更新的 > 旧的
高置信 > 低置信
高重要性 > 低重要性
```

示例：

```txt
长期偏好：用户喜欢川菜。
临时约束：用户今天胃不舒服，少辣。
```

当天推荐晚餐时，Memory Service 应返回“少辣”作为更高优先级约束，同时可以保留“喜欢川菜”作为低优先级偏好。

### 写入示例

用户说：

```txt
以后做饭别给我推荐香菜，我喜欢少油少辣。
```

同步写入：

```json
{
  "role": "user",
  "content": "以后做饭别给我推荐香菜，我喜欢少油少辣。"
}
```

异步抽取后写入两个节点：

```json
{
  "scope": "user",
  "domain": "生活技能",
  "categoryPath": ["生活技能", "做饭", "饮食偏好"],
  "type": "preference",
  "title": "用户不吃香菜",
  "content": "推荐菜谱时避免香菜。",
  "status": "active",
  "confidence": 0.98,
  "importance": 0.9,
  "tags": ["忌口", "菜谱推荐"],
  "entities": ["香菜"]
}
```

```json
{
  "scope": "user",
  "domain": "生活技能",
  "categoryPath": ["生活技能", "做饭", "饮食偏好"],
  "type": "preference",
  "title": "用户偏好少油少辣",
  "content": "推荐菜谱时优先少油少辣。",
  "status": "active",
  "confidence": 0.95,
  "importance": 0.85,
  "tags": ["口味", "菜谱推荐"],
  "entities": ["少油", "少辣"]
}
```

### 读取示例

用户下一次问：

```txt
今晚吃什么？
```

检索条件：

```txt
domain = 生活技能
categoryPath startsWith 生活技能/做饭
types = preference, constraint, recipe
status = active
```

组装给模型的上下文片段：

```txt
用户做饭偏好：
- 推荐菜谱时避免香菜。
- 推荐菜谱时优先少油少辣。
```

模型最终推荐时就不需要知道完整历史，只需要这两条结构化记忆。

### 更新示例

用户后来补充：

```txt
香菜现在可以接受一点，但不要太多。
```

处理方式：

```txt
旧节点：用户不吃香菜 -> status = superseded
新节点：用户可接受少量香菜 -> status = active, supersedesId = oldNodeId
```

这样系统不会丢掉历史，也不会继续按旧偏好推荐。

## SQLite 落地方案

第一版推荐使用 SQLite。它比 JSONL 更适合做查询、索引、事务和状态更新，同时不需要引入服务端数据库。

### 文件位置

建议根据记忆 scope 分开落盘：

```txt
workspace scope: <workspace>/.ekko/memory.db
user scope:      ~/.ekko/memory.db
session scope:   可以存入 workspace db，也可以存入服务端会话库
global scope:    随包发布的只读 seed 数据，或服务端同步
```

如果第一版只做 workspace 本地记忆，可以先统一使用：

```txt
<workspace>/.ekko/memory.db
```

### 表结构

SQLite 不需要把所有数组拆成多张表。第一版可以把数组字段存成 JSON text，常用筛选字段单独建列。

```sql
create table if not exists memory_messages (
  id text primary key,
  session_id text not null,
  parent_id text,
  role text not null,
  content text not null,
  metadata_json text,
  created_at text not null
);

create table if not exists memory_summaries (
  id text primary key,
  session_id text not null,
  parent_summary_id text,
  from_message_id text not null,
  to_message_id text not null,
  summary text not null,
  current_goal text,
  constraints_json text not null default '[]',
  preferences_json text not null default '[]',
  decisions_json text not null default '[]',
  completed_work_json text not null default '[]',
  pending_work_json text not null default '[]',
  known_issues_json text not null default '[]',
  created_at text not null
);

create table if not exists memory_nodes (
  id text primary key,
  parent_id text,
  supersedes_id text,
  session_id text,
  workspace_id text,
  user_id text,
  scope text not null,
  domain text not null,
  category_path_json text not null,
  category_path_text text not null,
  type text not null,
  key text,
  value_json text,
  title text not null,
  content text not null,
  status text not null,
  confidence real not null,
  importance real not null,
  tags_json text not null default '[]',
  entities_json text not null default '[]',
  source_message_ids_json text not null default '[]',
  created_at text not null,
  updated_at text not null,
  expires_at text
);

create table if not exists memory_audit_events (
  id text primary key,
  event_type text not null,
  node_id text,
  session_id text,
  workspace_id text,
  user_id text,
  actor text not null,
  reason text not null,
  payload_json text,
  created_at text not null
);
```

`category_path_text` 用 `/` 拼接，方便做前缀查询：

```txt
生活技能/做饭/饮食偏好
```

### 索引

```sql
create index if not exists idx_memory_messages_session_created
  on memory_messages (session_id, created_at);

create index if not exists idx_memory_summaries_session_created
  on memory_summaries (session_id, created_at);

create index if not exists idx_memory_nodes_lookup
  on memory_nodes (scope, status, domain, type, importance, updated_at);

create index if not exists idx_memory_nodes_key
  on memory_nodes (scope, status, domain, type, key, updated_at);

create index if not exists idx_memory_nodes_category
  on memory_nodes (category_path_text);

create index if not exists idx_memory_nodes_session
  on memory_nodes (session_id, status, updated_at);

create index if not exists idx_memory_nodes_workspace
  on memory_nodes (workspace_id, status, updated_at);

create index if not exists idx_memory_nodes_user
  on memory_nodes (user_id, status, updated_at);

create index if not exists idx_memory_audit_events_node
  on memory_audit_events (node_id, created_at);
```

可选增加 FTS5，用于关键词检索：

```sql
create virtual table if not exists memory_nodes_fts using fts5(
  title,
  content,
  tags,
  entities,
  content='memory_nodes',
  content_rowid='rowid'
);
```

如果使用 FTS，需要在 upsert memory node 时同步维护 FTS 表，或使用 trigger。

### 典型查询

取最近消息：

```sql
select *
from memory_messages
where session_id = ?
order by created_at desc
limit ?;
```

取最新摘要：

```sql
select *
from memory_summaries
where session_id = ?
order by created_at desc
limit 1;
```

取做饭相关 active 记忆：

```sql
select *
from memory_nodes
where status = 'active'
  and domain = '生活技能'
  and category_path_text like '生活技能/做饭/%'
  and (expires_at is null or expires_at > ?)
order by importance desc, confidence desc, updated_at desc
limit ?;
```

取当前 workspace 的约束和决策：

```sql
select *
from memory_nodes
where status = 'active'
  and workspace_id = ?
  and type in ('constraint', 'decision', 'fact')
  and (expires_at is null or expires_at > ?)
order by importance desc, updated_at desc
limit ?;
```

### 事务写入

一次记忆整理应该放在事务里：

```txt
begin
  insert summary if needed
  insert new nodes
  update superseded old nodes
  update fts index if enabled
commit
```

这样可以避免出现“新记忆写入了，但旧记忆没失效”的半完成状态。

每次 create、update、supersede、expire、delete 都应该追加一条 `memory_audit_events`。审计事件不参与模型上下文注入，只用于调试、用户解释和回滚。

### 迁移

建议维护一个简单版本表：

```sql
create table if not exists memory_schema_migrations (
  version integer primary key,
  applied_at text not null
);
```

Ekko Agent 启动 memory store 时：

```txt
打开 SQLite
读取 migration version
按顺序执行缺失 migration
创建索引
返回 SqliteMemoryStore
```

## 受控 Schema

精确检索依赖稳定 schema。不能让模型自由发明 `key`，否则同一个意思可能被写成 `avoid_food`、`avoid_ingredient`、`excluded_ingredient`，后续 SQL 很难稳定命中。

建议按 domain 维护受控 schema：

```ts
type MemorySchemaRule = {
  domain: string
  categoryPathPrefix?: string[]
  type: MemoryNode['type']
  allowedKeys: string[]
  valueShape: 'string' | 'number' | 'boolean' | 'string[]' | 'object'
  defaultScope: MemoryNode['scope']
  requiresExplicitUserIntent: boolean
}
```

做饭偏好示例：

```ts
const cookingPreferenceSchema = {
  domain: '生活技能',
  categoryPathPrefix: ['生活技能', '做饭'],
  type: 'preference',
  allowedKeys: [
    'avoid_ingredient',
    'preferred_ingredient',
    'flavor_profile',
    'dietary_restriction',
    'cooking_time_preference',
  ],
  valueShape: 'object',
  defaultScope: 'user',
  requiresExplicitUserIntent: true,
}
```

Memory Service 在写入前需要做 normalization：

```txt
同义 key 归一：disliked_ingredient -> avoid_ingredient
中文值归一：少辣 -> { spicy: 'low' }
scope 归一：项目技术约束默认 workspace，不默认 user
status 归一：没有 expiresAt 的临时状态不能进入长期 user-scope
```

模型抽取结果如果不符合受控 schema，应被拒绝或降级为 session-scope 临时记忆。

## 权限和确认策略

不同 scope 的写入风险不同：

```txt
session: 默认允许，适合临时状态和当前任务。
workspace: 允许明确项目事实、技术约束、任务状态。
user: 只允许用户明确表达的长期偏好和稳定事实。
global: 第一版不允许 agent 写入。
```

需要确认的情况：

- 写入 user-scope 长期记忆，但用户没有使用“记住”“以后都”等明确表达。
- 删除命中多条记忆。
- hard delete。
- 写入或删除敏感信息。
- 覆盖高置信历史记忆，但新消息表达不够明确。

不需要确认的情况：

- session-scope 临时记忆。
- 用户明确要求记录的低风险偏好。
- workspace-scope 的当前任务状态。
- soft delete 单条精确命中的记忆。

## 失败和降级

记忆系统不能阻塞主对话。

失败处理：

```txt
SQLite 打不开：跳过记忆检索，主 Agent 正常回答，并记录 runtime warning。
检索失败：只使用最近消息和当前输入。
抽取失败：不重试阻塞当前回复，放入后台 retry queue。
写入失败：告诉主 Agent 写入未成功，不要回复“记住了”。
迁移失败：禁用 memory store，避免写入未知 schema。
```

`MemoryContext` 应包含诊断字段：

```ts
type MemoryContextDiagnostics = {
  enabled: boolean
  storeStatus: 'ok' | 'disabled' | 'degraded'
  warnings: string[]
  retrievedNodeCount: number
  omittedNodeCount: number
}
```

## 可观测性和解释

每次回复最好保留本轮使用过的记忆 id，但不一定展示给用户。

```ts
type MemoryContext = {
  latestSummary?: MemorySummary
  recentMessages: MemoryMessage[]
  activeTasks: MemoryNode[]
  relevantNodes: MemoryNode[]
  constraints: MemoryNode[]
  preferences: MemoryNode[]
  usedMemoryIds: string[]
  diagnostics: MemoryContextDiagnostics
}
```

当用户问“你为什么这么推荐”时，主 Agent 可以基于 `usedMemoryIds` 解释：

```txt
因为我记录到你做饭偏好少油少辣，并且不希望推荐香菜。
```

解释时不要暴露内部 SQL、embedding 分数或无关历史消息。

## 测试验收

第一版至少覆盖这些测试：

- append message 后可以按 session 取最近消息。
- append summary 后可以取最新摘要。
- create node 后 active 查询能命中。
- `key/valueJson` 精确查询能命中做饭偏好。
- supersede 后旧节点不再进入默认上下文。
- expiresAt 过期后不再注入。
- soft delete 后不再检索。
- hard delete 后节点、FTS、embedding 记录被清理。
- 命中多条删除候选时返回 `requiresConfirmation`。
- SQLite 事务失败时不会出现半写入状态。
- memory store 失败时主 Agent 能降级继续运行。
- schema normalization 能把同义 key 归一到受控 key。

## 分类体系

分类树用于归档，记忆节点用于表达事实和演化。不要把目录树当成全部记忆。

示例：

```txt
生活技能
  做饭
    菜谱
    食材处理
    厨房工具
    烹饪技巧
    营养搭配
    采购清单
    饮食偏好
  家务
  理财
  健康
  出行
```

一条记忆可以挂在一个主分类下，同时用 tags 和 entities 表达交叉关系。

示例：

```json
{
  "domain": "生活技能",
  "categoryPath": ["生活技能", "做饭", "饮食偏好"],
  "type": "preference",
  "title": "用户不吃香菜",
  "content": "用户明确表示以后不要推荐带香菜的菜。",
  "tags": ["做饭", "菜谱推荐", "忌口"],
  "entities": ["香菜"],
  "confidence": 0.98
}
```

菜谱示例：

```json
{
  "domain": "生活技能",
  "categoryPath": ["生活技能", "做饭", "菜谱"],
  "type": "recipe",
  "title": "番茄炒蛋",
  "content": "家常快手菜，主要食材是番茄和鸡蛋。可按用户偏好少油、不放香菜。",
  "tags": ["家常菜", "快手菜", "少辣"],
  "entities": ["番茄", "鸡蛋"]
}
```

## 抽取流程

在 agent 回复后异步执行记忆整理。

触发条件：

- 新增 6-10 条消息。
- 新增上下文超过指定 token 阈值。
- 用户表达明确偏好、决策、纠正或长期目标。
- 任务状态发生变化。

流程：

```txt
新增消息
  -> 保存原始日志
  -> 读取上一条摘要
  -> 抽取结构化候选记忆
  -> 判断新增、更新、覆盖或过期
  -> 写入 MemoryNode
  -> 必要时生成 MemorySummary
```

抽取器输出建议：

```ts
type MemoryExtraction = {
  summaryPatch?: string
  nodes: Array<{
    operation: 'create' | 'update' | 'supersede' | 'expire' | 'ignore'
    targetId?: string
    node: Partial<MemoryNode>
    reason: string
  }>
}
```

## 覆盖和冲突

用户纠正优先级高于历史记忆。

示例：

```txt
旧记忆：用户喜欢吃辣。
新消息：最近胃不舒服，先别推荐辣的。
```

处理方式：

- 不一定删除旧记忆。
- 新建一条临时偏好，`status = active`。
- 旧记忆仍保留，但检索注入时被新记忆压过。
- 如果用户明确说“以后都不吃辣”，再 supersede 旧记忆。

## 回复前上下文组装

每次模型回复前只注入最小必要记忆。

优先级：

```txt
当前用户消息
> 最近几轮原始消息
> 当前任务状态
> 最新摘要
> 高置信结构化记忆
> 相关历史记忆
> 低置信或过期记忆
```

上下文包：

```ts
type MemoryContext = {
  latestSummary?: MemorySummary
  recentMessages: MemoryMessage[]
  activeTasks: MemoryNode[]
  relevantNodes: MemoryNode[]
  constraints: MemoryNode[]
  preferences: MemoryNode[]
}
```

建议限制：

- 最新摘要：1 条。
- 最近消息：3-8 条。
- 结构化记忆：最多 10-20 条。
- 单条记忆内容保持短文本。

## Token 成本策略

- 不每轮抽取，按阈值批处理。
- 抽取只看上一条摘要和新增消息。
- 记忆抽取可使用便宜模型或本地小模型。
- 回复时只注入相关记忆，不注入完整记忆库。
- 已完成任务和临时日志降低重要性或设置过期时间。

## 第一版落地建议

### Phase 1: 本地结构化记忆

- 增加 memory 模块。
- 保存 messages、summaries、nodes。
- 支持基础分类、状态、来源追踪。
- 实现基于规则和模型 JSON 输出的抽取接口。
- 回复前注入最近摘要和 active nodes。

### Phase 2: 检索和覆盖

- 增加关键词搜索。
- 增加 `supersedesId` 和冲突处理。
- 增加 task/preference/decision 的专用 upsert 逻辑。
- 增加记忆重要性和过期机制。

### Phase 3: 向量和图关系

- 对 MemoryNode 生成 embedding。
- 增加 topK 语义召回。
- 抽取 entities 和 relationships。
- 支持 temporal graph 查询。

## 与 Ekko Agent 当前模块的关系

建议新增目录：

```txt
packages/ekko-agent/src/memory/
  context.ts
  extraction.ts
  store.ts
  types.ts
  retrieval.ts
```

职责：

- `types.ts`: 记忆数据结构。
- `store.ts`: 存储接口和 SQLite 实现。
- `extraction.ts`: 从消息中抽取结构化记忆。
- `retrieval.ts`: 根据当前输入检索相关记忆。
- `context.ts`: 组装给模型的 MemoryContext。

`AgentRuntime` 后续可以在两个位置接入：

- 回复前：调用 retrieval/context，把记忆注入系统提示或上下文消息。
- 回复后：调用 extraction/store，异步更新记忆。

## 开放问题

- 记忆存储落在哪里：服务端数据库、workspace 文件，还是用户级 profile。
- 哪些记忆需要用户可见和可编辑。
- 是否允许 agent 自动写入 user-scope 长期记忆。
- 记忆抽取使用哪个模型和预算。
- 是否需要多租户隔离和加密。
