7 Commits

Author SHA1 Message Date
150e2d5607 docs: 修正文档与代码实现的差异
以代码为准,修正以下内容:
1. Actions 表格添加 task_link_info 操作
2. recall 参数补充 depth 字段
3. 导入路径更新:waterflow -> waterflow-ts/dist/...
4. installTrulyMEM 需要 await(返回 Promise)
5. 新增 API 名称映射章节(mapToolIdToApiName/mapApiNameToToolId)
6. 新增 task_link_info 使用示例
2026-04-17 14:07:04 +08:00
38941ff2b5 fix: 修复 sql.js Uint8Array 与 fs.writeFile 的兼容性问题
sql.js export() 返回 Uint8Array,需用 Buffer.from() 转换后再写入文件
解决潜在的类型不匹配错误
2026-04-17 13:47:14 +08:00
53d6940994 fix: 修复 AI 工具调用的两个问题
问题1: AI 生成的参数格式与工具期望不一致
- 改进工具 description,添加 commit 操作的三元组格式说明和示例
- 完善 input_schema,详细描述 triplets 的 subject/relation/object 字段
- 在 description 中明确说明必填字段

问题2: 工具名称格式不符合 API 要求
- OpenAI/DeepSeek API 要求工具名称符合 ^[a-zA-Z0-9_-]+$
- 内部 ID "builtin:graph_memory" 含冒号,不符合要求
- 添加 apiName 属性提供 API 兼容名称 "graph_memory"
- 添加 mapToolIdToApiName/mapApiNameToToolId 映射函数

同时修正 import 路径:waterflow/... -> waterflow-ts/dist/...
2026-04-17 13:08:18 +08:00
16327dec72 docs: 更新README - 修正Skill名称并添加Skill定义格式说明
- 修正Skill列表中的名称:persona/task(WaterFlow使用目录名)
- 添加Skill定义格式说明,解释WaterFlow如何解析SKILL.md
- 说明name/description/allowed-tools等字段的提取规则
- 中英文版本同步更新
2026-04-16 17:25:24 +08:00
9e2f390332 fix: 修复Skill定义兼容性 - 整合description和when_to_use到Markdown body
移除frontmatter中不被WaterFlow处理的冗余字段:
- name: WaterFlow使用目录名作为skill名称
- description: WaterFlow从Markdown #标题提取
- when_to_use: 不被处理,整合到body中

修改后的SKILL.md完全兼容WaterFlow SkillLoader:
- description从#标题正确提取
- allowed-tools正确转换为allowedTools
- arguments正确映射
- user-invocable正确转换为userInvocable
2026-04-16 17:22:56 +08:00
56e787a5a8 fix: 修正 WaterFlow Skill/Tool 兼容性问题 + 类型声明完善
Skill 格式修正:
- allowed_tools → allowed-tools (kebab-case)
- user_invocable → user-invocable (kebab-case)
- persona skill name: graph_memory_persona → persona (匹配目录名)
- task skill name: graph_memory_task → task (匹配目录名)

Tool 接口优化:
- installTrulyMEM: require() → ESM dynamic import
- handler: 添加 abort 信号检查
- graph_database: _platform 类型 any → Platform
- graph_database: 修复 fs null 检查和 ArrayBuffer 类型转换

类型声明完善:
- waterflow.d.ts 从 193 行扩展到 400+ 行
- 添加 SchemaType, ToolFeatures, ToolMetadata 等完整类型
- 添加 network, tools, builtinTools 等缺失字段
- 添加 waterflow/runtime/core/tools/builtin 模块声明
- 添加 waterflow/runtime/core/tools/tool_registry 模块声明

清理:
- 删除 TRACKING.md, 重构.md, migration_plan.md, todo_progress.md
- 删除 docs/integration/waterflow-design.md
2026-04-16 15:16:55 +08:00
13e3c662ac refactor: adapt to WaterFlow framework - zero source changes required
- Remove duplicate platform layer and tool interface definitions
- Add BFS breadth-first search with depth annotation (sync from main)
- Use WaterFlow platform.fs for binary storage instead of custom storage
- Add installTrulyMEM() one-line registration function
- Update SKILL.md with 10 complete operations
- Add waterflow.d.ts type declarations
- Update bilingual README with simplified installation guide
- Build passes with 0 errors
2026-04-16 11:48:58 +08:00
48 changed files with 3364 additions and 8983 deletions

4
.gitignore vendored
View File

@ -70,7 +70,3 @@ node_modules/
dist/
*.tsbuildinfo
tsconfig.tsbuildinfo
# Task Archive (runtime generated)
task_archive/
ts/task_archive/

View File

@ -1,29 +0,0 @@
# 修复追踪文件
**分支**: openclaw
**开始时间**: 2026-04-16
**状态**: 重新执行
---
## 修复计划
### P0 - 阻断性问题
#### 1. 删除自定义 tool_interface.ts
#### 2. 重构 graph_memory_tool.ts
#### 3. 更新 plugin-entry.ts
### P2 - 最佳实践
#### 4. 重组 Skill 目录结构
#### 5. 更新 bundled-skills 目录结构
#### 6. 更新 openclaw.plugin.json
#### 7. 更新 README.md / README_EN.md
---
## 执行记录
### [进行中] P0: 核心修复

View File

@ -1,117 +0,0 @@
# TrulyMEM → WaterFlow 迁移计划
> ⚠️ **修改只在 TrulyMEM 的 waterflow 分支执行** ⚠️
>
> 所有代码修改仅应用于 TrulyMEM 仓库的 `waterflow` 分支,作为 WaterFlow 框架的适配版本。
---
## 迁移目标
将 TrulyMEM 的图记忆能力从 Python 迁移到 TypeScript适配 WaterFlow 框架。
**代码位置**: `/home/program/TrulyMEM-TrueHumanMEM/` (waterflow 分支)
---
## 迁移策略
将图记忆能力作为 TypeScript 模块添加到 waterflow 分支:
| TrulyMEM (Python) | WaterFlow (TypeScript) |
|-------------------|------------------------|
| `EmbeddedGraphDB` | `GraphDatabase` |
| `GraphMemoryClient` | `MemoryService` |
| 12 个记忆工具 | `GraphMemoryTool` + Skills |
---
## 实施步骤
### Phase 1: 项目结构
- [x] 1.1 创建 `ts/` 目录 - TypeScript 项目
- [x] 1.2 创建 `package.json` - 项目配置
- [x] 1.3 创建 `tsconfig.json` - TypeScript 配置
### Phase 2: 核心库
- [x] 2.1 创建 `ts/src/runtime/core/graph_memory/types.ts` - 类型定义
- [x] 2.2 创建 `ts/src/runtime/core/graph_memory/graph_database.ts` - 图数据库
- [x] 2.3 创建 `ts/src/runtime/core/graph_memory/memory_service.ts` - 记忆服务
- [x] 2.4 创建 `ts/src/runtime/core/graph_memory/index.ts` - 模块导出
### Phase 3: Tool 接口
- [x] 3.1 创建 `ts/src/runtime/core/tools/builtin/graph_memory_tool.ts` - Tool 实现
- [x] 3.2 注册 Tool (作为独立模块导出)
### Phase 4: Skill 定义
- [x] 4.1 创建 `ts/bundled-skills/graph_memory/SKILL.md` - 主 Skill
- [x] 4.2 创建 `ts/bundled-skills/graph_memory/persona/SKILL.md` - Persona
- [x] 4.3 创建 `ts/bundled-skills/graph_memory/task/SKILL.md` - 任务管理
### Phase 5: 验证
- [x] 5.1 编译 TypeScript - 无错误
- [ ] 5.2 运行测试
---
## 目录结构 (在 TrulyMEM waterflow 分支)
```
TrulyMEM-TrueHumanMEM/
├── ts/ # TypeScript 项目 (保留)
│ ├── src/
│ │ └── runtime/core/
│ │ ├── graph_memory/ # 图记忆模块
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ ├── graph_database.ts
│ │ │ └── memory_service.ts
│ │ └── tools/
│ │ └── builtin/
│ │ └── graph_memory_tool.ts
│ ├── bundled-skills/
│ │ └── graph_memory/
│ │ ├── SKILL.md
│ │ ├── persona/SKILL.md
│ │ └── task/SKILL.md
│ ├── package.json
│ └── tsconfig.json
├── docs/integration/waterflow-design.md # 迁移设计文档 (保留)
└── (其他文件迁移后删除)
```
---
## 迁移后清理
迁移完成后waterflow 分支将删除以下文件:
- `core/` - Python 核心代码
- `ui/` - Python UI 代码
- `tests/` - Python 测试
- `tools/` - Python 工具
- `trulymem_entry.py` - Python 入口
- `build/` - 构建脚本
- `pic/` - 图片资源 (除图标外)
- `requirements.txt` - Python 依赖
- `TrulyMEM.spec` - Python 打包配置
只保留:
- `ts/` - TypeScript 源码
- `docs/integration/waterflow-design.md` - 迁移文档
- `.gitignore`, `LICENSE`
---
## 工作追踪
工作进度记录在: `todo_progress.md`
每次修改文件前后请查看此文件并更新进度。

View File

@ -1,572 +0,0 @@
# TrulyMEM → OpenClaw 重构修复计划
> **分支**: `openclaw`
> **代码位置**: `/home/program/TrulyMEM-TrueHumanMEM/`
> **目标**: 将当前 TypeScript 实现完全适配 OpenClaw 框架的接口规范,并补全 main 分支缺失的功能
---
## 背景分析
### 当前状态
openclaw 分支已完成骨架迁移类型定义、核心类GraphDatabase/MemoryService/GraphMemoryTool、Skill 定义文件均已就位TypeScript 编译通过。
### 核心问题
| 类别 | 问题 | 严重度 |
|---|---|---|
| **接口不兼容** | Tool 注册使用自定义 interface非 OpenClaw Plugin SDK | 🔴 致命 |
| **Skill 格式错误** | 多行 YAML 嵌套结构(`arguments``allowed_tools`OpenClaw 解析器只支持单行键值 | 🔴 致命 |
| **无持久化** | in-memory Map进程重启后记忆全部丢失 | 🔴 致命 |
| **Schema 格式** | 手写 JSON Schema 对象,非 `@sinclair/typebox` | 🔴 致命 |
| **返回值格式** | `JSON.stringify({success, data})``{ content: [{ type: "text", text }] }` | 🔴 致命 |
| **无 Plugin 结构** | 缺少 `openclaw.plugin.json``package.json``openclaw` 字段 | 🔴 致命 |
| **功能缺失** | depth 遍历、timeRange 过滤、supersede 模式、archive、cleanup、ToolLimiter | 🟡 中等 |
| **无测试** | 全部删除,无新测试覆盖 | 🔴 严重 |
| **命名不规范** | `graph_memory`(下划线)应为 kebab-case | 🟡 轻微 |
---
## OpenClaw 接口规范对照
### Tool 注册(官方 Plugin SDK
```typescript
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { Type } from "@sinclair/typebox";
export default definePluginEntry({
id: "graph-memory",
register(api) {
api.registerTool({
name: "graph_memory",
description: "图记忆工具 - 让 AI 拥有真正的长期记忆能力",
parameters: Type.Object({
action: Type.String({ enum: ["recall", "commit", "purge", ...] }),
params: Type.Object({ ... }),
}),
async execute(_id, params) {
// 返回格式必须是:
return { content: [{ type: "text", text: resultString }] };
},
});
},
});
```
### SKILL.md 格式(官方要求)
```markdown
---
name: graph-memory
description: "图记忆工具 - 检索、写入、删除记忆,管理人设和任务"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# 正文指令...
```
**关键约束**
- `metadata` 必须是**单行 JSON 对象**
- `description` 不能包含 `: `(冒号+空格),否则 YAML 解析**静默失败**
- `name` 必须 kebab-case与文件夹名匹配
- **不支持** `arguments``allowed_tools``context: inline` 等多行 YAML 嵌套结构
- 所有 frontmatter 键值必须是**单行**
### Plugin Manifest
```json
{
"id": "graph-memory",
"name": "Graph Memory",
"description": "让 AI 拥有真正的长期记忆能力",
"configSchema": {
"type": "object",
"additionalProperties": false
}
}
```
### package.json 扩展
```json
{
"openclaw": {
"extensions": ["./dist/plugin-entry.js"],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
}
}
}
```
---
## 实施步骤
### Phase 1: 项目结构改造为 OpenClaw Plugin🔴 P0 - 阻塞)
#### 1.1 添加 OpenClaw Plugin SDK 依赖
- [ ] 1.1.1 `npm install @sinclair/typebox`
- [ ] 1.1.2 更新 `ts/package.json` 添加 `openclaw` 字段extensions、compat、build
- [ ] 1.1.3 创建 `ts/openclaw.plugin.json` manifest 文件
#### 1.2 创建 Plugin Entry Point
- [ ] 1.2.1 创建 `ts/src/plugin-entry.ts`
- 使用 `definePluginEntry` 包裹
- 通过 `api.registerTool()` 注册 GraphMemoryTool
- 工具名称: `graph_memory`
- 描述: "图记忆工具 - 让 AI 拥有真正的长期记忆能力"
- [ ] 1.2.2 确保 entry point 导出为 ESM 格式
- [ ] 1.2.3 更新 `ts/tsconfig.json` 确保编译输出路径正确
#### 1.3 迁移 Tool Schema 到 TypeBox
- [ ] 1.3.1 创建 `ts/src/runtime/core/tools/builtin/graph_memory_schema.ts`
-`Type.Object` 定义 action 参数
-`Type.Object` 定义 params 嵌套结构
- 覆盖所有 10 个 action 的参数类型
- [ ] 1.3.2 更新 `graph_memory_tool.ts``inputSchema` 字段为 TypeBox schema
- [ ] 1.3.3 修改 `handler` 方法签名匹配 OpenClaw 的 `execute(_id, params)` 格式
- [ ] 1.3.4 修改返回值格式为 `{ content: [{ type: "text", text: string }] }`
#### 1.4 更新 Tool Interface
- [ ] 1.4.1 更新 `ts/src/runtime/core/tools/tool_interface.ts`
- 保持向后兼容(如其他模块引用)
- 添加 OpenClaw 兼容的 `execute` 方法签名
- 添加 `content` 返回类型定义
#### Phase 1 验收标准
- [ ] `npm run build` 编译通过
- [ ] `openclaw.plugin.json` 格式正确
- [ ] Plugin entry 使用 `definePluginEntry`
- [ ] Tool 使用 `api.registerTool` 注册
- [ ] Schema 使用 TypeBox
- [ ] 返回值格式为 `{ content: [{ type: "text", text }] }`
---
### Phase 2: SKILL.md 格式修复(🔴 P0 - 阻塞)
#### 2.1 修复 `skills/graph_memory/SKILL.md`
- [ ] 2.1.1 移除 `when_to_use` 多行值(合并到 `description`
- [ ] 2.1.2 移除 `context: inline`(非 OpenClaw 标准字段)
- [ ] 2.1.3 移除 `allowed_tools` 多行数组
- [ ] 2.1.4 移除 `arguments` 多行嵌套结构
- [ ] 2.1.5 `name` 改为 `graph-memory`kebab-case
- [ ] 2.1.6 `description` 改为单行,不含 `: `
- [ ] 2.1.7 添加 `metadata` 单行 JSON
- [ ] 2.1.8 保留 `user_invocable: true`(改为 `user-invocable: true`kebab-case
**修复后格式**
```yaml
---
name: graph-memory
description: "图记忆工具 - 检索、写入、删除记忆,管理人设和任务。使用 recall 检索、commit 写入、purge 删除"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
```
#### 2.2 修复 `skills/graph_memory/persona/SKILL.md`
- [ ] 2.2.1 移除 `when_to_use``context``allowed_tools``arguments`
- [ ] 2.2.2 `name` 改为 `graph-memory-persona`
- [ ] 2.2.3 `description` 改为单行
- [ ] 2.2.4 添加 `metadata` 单行 JSON
#### 2.3 修复 `skills/graph_memory/task/SKILL.md`
- [ ] 2.3.1 移除 `when_to_use``context``allowed_tools``arguments`
- [ ] 2.3.2 `name` 改为 `graph-memory-task`
- [ ] 2.3.3 `description` 改为单行
- [ ] 2.3.4 添加 `metadata` 单行 JSON
#### 2.4 同步修复 `ts/bundled-skills/` 下三个文件
- [ ] 2.4.1 `ts/bundled-skills/graph_memory/SKILL.md`
- [ ] 2.4.2 `ts/bundled-skills/graph_memory/persona/SKILL.md`
- [ ] 2.4.3 `ts/bundled-skills/graph_memory/task/SKILL.md`
#### 2.5 重命名目录kebab-case
- [ ] 2.5.1 `skills/graph_memory/``skills/graph-memory/`
- [ ] 2.5.2 `skills/graph_memory/persona/``skills/graph-memory/persona/`
- [ ] 2.5.3 `skills/graph_memory/task/``skills/graph-memory/task/`
- [ ] 2.5.4 `ts/bundled-skills/graph_memory/``ts/bundled-skills/graph-memory/`
- [ ] 2.5.5 同步更新 README.md 和 README_EN.md 中的路径引用
#### Phase 2 验收标准
- [ ] 所有 SKILL.md 的 frontmatter 仅含单行键值
- [ ] `name` 全部 kebab-case
- [ ] `description` 不含 `: `
- [ ] `metadata` 为单行 JSON 对象
- [ ]`arguments``allowed_tools``context: inline` 等非标准字段
- [ ] 目录名与 `name` 一致
---
### Phase 3: GraphDatabase 持久化(🔴 P0 - 核心价值)
#### 3.1 添加 SQLite 依赖
- [ ] 3.1.1 `npm install better-sqlite3`
- [ ] 3.1.2 `npm install @types/better-sqlite3 --save-dev`
#### 3.2 重写 GraphDatabase
- [ ] 3.2.1 修改构造函数接受 `dbPath` 参数
- [ ] 3.2.2 使用 `better-sqlite3` 创建/连接数据库
- [ ] 3.2.3 创建实体表entities
- `id TEXT PRIMARY KEY`
- `name TEXT UNIQUE NOT NULL`
- `type TEXT`
- `mention_count INTEGER DEFAULT 1`
- `created_at TEXT DEFAULT CURRENT_TIMESTAMP`
- `updated_at TEXT DEFAULT CURRENT_TIMESTAMP`
- [ ] 3.2.4 创建关系表relations
- `id TEXT PRIMARY KEY`
- `source_id TEXT NOT NULL`(外键 → entities.id
- `target_id TEXT NOT NULL`(外键 → entities.id
- `relation_type TEXT NOT NULL`
- `confidence REAL DEFAULT 1.0`
- `status TEXT DEFAULT 'active'`
- `session_id TEXT`
- `turn_id INTEGER`
- `created_at TEXT DEFAULT CURRENT_TIMESTAMP`
- `updated_at TEXT DEFAULT CURRENT_TIMESTAMP`
- `date_bucket TEXT`
- `superseded_by INTEGER`
- [ ] 3.2.5 创建索引:
- `idx_entity_name ON entities(name)`
- `idx_entity_type ON entities(type)`
- `idx_relation_source ON relations(source_id)`
- `idx_relation_target ON relations(target_id)`
- `idx_relation_type ON relations(relation_type)`
- `idx_relation_status ON relations(status)`
- `idx_relation_session ON relations(session_id)`
- `idx_relation_date ON relations(date_bucket)`
#### 3.3 实现 recall 的 depth 多跳遍历
- [ ] 3.3.1 实现 BFS/DFS 图遍历算法
- [ ] 3.3.2 depth=1: 直接匹配关键词的实体及其关系
- [ ] 3.3.3 depth=2: 扩展到相邻实体的关系
- [ ] 3.3.4 depth=N: 递归扩展到 N 层
- [ ] 3.3.5 限制最大 depth 为 5防止爆炸
- [ ] 3.3.6 去重已访问实体
#### 3.4 实现 recall 的 timeRange 过滤
- [ ] 3.4.1 解析 `timeRange.days` 参数
- [ ] 3.4.2 在 SQL 查询中添加 `created_at >= datetime('now', '-N days')` 条件
- [ ] 3.4.3 支持 `timeRange.from``timeRange.to` 范围查询
#### 3.5 实现 purge 的 supersede 模式
- [ ] 3.5.1 当 `mode === 'supersede'` 时:
- 标记旧关系为 `superseded`
- 设置 `superseded_by` 指向新关系 ID
- 创建新关系(使用 `newRelation` 参数)
- [ ] 3.5.2 更新 `PurgeParams` 类型支持 `newRelation` 字段
#### 3.6 实现 memory_archive 归档功能
- [ ] 3.6.1 添加 `archive(days: number)` 方法
- [ ] 3.6.2 将 N 天前的非活跃关系标记为 `archived`
- [ ] 3.6.3 在 recall 中排除 `archived` 状态的关系(除非显式查询)
#### 3.7 实现 memory_cleanup 清理功能
- [ ] 3.7.1 添加 `cleanup(dryRun: boolean)` 方法
- [ ] 3.7.2 物理删除 `status = 'deleted'` 超过 90 天的关系
- [ ] 3.7.3 删除孤立节点(无任何关系连接的实体)
- [ ] 3.7.4 `dryRun=true` 时只返回将被删除的内容
#### 3.8 修复 isEntityDeleted 逻辑
- [ ] 3.8.1 当前逻辑有误:只要有一个关系被删就算实体被删
- [ ] 3.8.2 修正为:实体本身无 deleted 状态,通过关系状态判断
- [ ] 3.8.3 或者:在 entities 表中添加 `status` 字段
#### Phase 3 验收标准
- [ ] 数据持久化:写入后重启进程,数据仍然存在
- [ ] recall depth 遍历正确返回 N 层关系
- [ ] timeRange 过滤按时间正确筛选
- [ ] supersede 模式正确标记替代关系
- [ ] archive 正确归档旧数据
- [ ] cleanup 正确清理无效数据
- [ ] 编译通过,无类型错误
---
### Phase 4: ToolLimiter 迁移(🟡 P2 - 优化)
#### 4.1 创建 ToolLimiter
- [ ] 4.1.1 创建 `ts/src/runtime/core/tools/tool_limiter.ts`
- [ ] 4.1.2 移植 Python `ToolLimiter` 逻辑:
- `ToolLimits` 配置类
- `ToolCallCount` 计数类
- `_classify_tool` 分类方法
- `can_call` 检查方法
- `record_call` 记录方法
- `reset` 重置方法
- [ ] 4.1.3 默认限制值:
- persona_query_max: 1
- persona_update_max: 1
- task_query_max: 4
- task_update_max: 5
- memory_query_max: 20
- memory_update_max: 10
#### 4.2 集成到 GraphMemoryTool
- [ ] 4.2.1 在 Tool 构造函数中初始化 ToolLimiter
- [ ] 4.2.2 在 `execute` 方法中调用 `can_call` 检查
- [ ] 4.2.3 调用成功后调用 `record_call` 记录
- [ ] 4.2.4 每轮对话结束时调用 `reset` 重置计数
#### Phase 4 验收标准
- [ ] ToolLimiter 正确分类所有工具调用
- [ ] 超过限制时返回明确的拒绝消息
- [ ] 每轮对话计数正确重置
---
### Phase 5: 测试重建(🔴 P1 - 质量保障)
#### 5.1 GraphDatabase 测试
- [ ] 5.1.1 创建 `ts/tests/runtime/core/graph_memory/graph_database.test.ts`
- [ ] 5.1.2 commit 测试:创建实体和关系
- [ ] 5.1.3 recall 测试:按关键词检索、按 seedEntities 检索
- [ ] 5.1.4 recall depth 测试1层、2层、3层遍历
- [ ] 5.1.5 recall timeRange 测试:按时间范围过滤
- [ ] 5.1.6 purge soft 测试:软删除
- [ ] 5.1.7 purge hard 测试:硬删除
- [ ] 5.1.8 purge supersede 测试:纠错替代
- [ ] 5.1.9 introspect 测试:返回统计
- [ ] 5.1.10 持久化测试:重启后数据保留
- [ ] 5.1.11 archive 测试:归档旧数据
- [ ] 5.1.12 cleanup 测试:清理无效数据
- [ ] 5.1.13 sessionFilter 测试:按会话过滤
- [ ] 5.1.14 并发测试:多线程安全
- [ ] 5.1.15 边界测试:空查询、超长字符串
#### 5.2 MemoryService 测试
- [ ] 5.2.1 创建 `ts/tests/runtime/core/graph_memory/memory_service.test.ts`
- [ ] 5.2.2 updatePersona merge 测试
- [ ] 5.2.3 updatePersona replace 测试
- [ ] 5.2.4 clearPersona 测试
- [ ] 5.2.5 createTask 测试
- [ ] 5.2.6 setTaskState 测试
- [ ] 5.2.7 deleteTask 测试
- [ ] 5.2.8 linkInfoToTask 测试
- [ ] 5.2.9 setSessionId/getSessionId 测试
- [ ] 5.2.10 任务状态转换测试(进行中→已暂停→进行中→已完成)
- [ ] 5.2.11 人设属性合并测试
- [ ] 5.2.12 错误处理测试:无效参数
#### 5.3 GraphMemoryTool 测试
- [ ] 5.3.1 创建 `ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts`
- [ ] 5.3.2 metadata 测试id、name、category
- [ ] 5.3.3 recall action 测试
- [ ] 5.3.4 commit action 测试
- [ ] 5.3.5 purge action 测试
- [ ] 5.3.6 introspect action 测试
- [ ] 5.3.7 persona_update action 测试
- [ ] 5.3.8 persona_clear action 测试
- [ ] 5.3.9 task_create action 测试
- [ ] 5.3.10 task_set_state action 测试
- [ ] 5.3.11 task_delete action 测试
- [ ] 5.3.12 task_link_info action 测试
- [ ] 5.3.13 未知 action 错误处理测试
- [ ] 5.3.14 返回值格式测试:`{ content: [{ type: "text", text }] }`
- [ ] 5.3.15 ToolLimiter 集成测试
#### 5.4 Plugin Entry 集成测试
- [ ] 5.4.1 创建 `ts/tests/plugin-entry.test.ts`
- [ ] 5.4.2 Plugin 注册测试
- [ ] 5.4.3 Tool 注册测试
- [ ] 5.4.4 Schema 验证测试
#### Phase 5 验收标准
- [ ] `npm test` 全部通过50+ 用例)
- [ ] 无跳过skip的测试
- [ ] 覆盖率 > 80%
---
### Phase 6: 文档更新(🟢 P3 - 收尾)
#### 6.1 更新 README.md
- [ ] 6.1.1 更新标题TrulyMEM → OpenClaw Graph Memory Plugin
- [ ] 6.1.2 更新安装方式:`openclaw plugins install`
- [ ] 6.1.3 更新使用示例
- [ ] 6.1.4 更新目录结构说明
- [ ] 6.1.5 更新 API 文档(反映 TypeBox schema
#### 6.2 更新 README_EN.md
- [ ] 6.2.1 同步中文 README 的所有更新
- [ ] 6.2.2 确保英文表达准确
#### 6.3 更新迁移设计文档
- [ ] 6.3.1 更新 `docs/integration/waterflow-design.md`
- [ ] 6.3.2 添加 OpenClaw 接口适配说明
- [ ] 6.3.3 更新架构图中 Plugin SDK 部分
#### Phase 6 验收标准
- [ ] README.md 和 README_EN.md 内容一致
- [ ] 安装步骤可执行
- [ ] API 文档与实际代码一致
---
## 目标目录结构(重构后)
```
TrulyMEM-TrueHumanMEM/
├── ts/
│ ├── src/
│ │ ├── plugin-entry.ts # [NEW] OpenClaw Plugin 入口
│ │ └── runtime/core/
│ │ ├── graph_memory/
│ │ │ ├── index.ts
│ │ │ ├── types.ts
│ │ │ ├── graph_database.ts # [REWRITE] SQLite 持久化
│ │ │ └── memory_service.ts
│ │ └── tools/
│ │ ├── builtin/
│ │ │ ├── graph_memory_tool.ts # [UPDATE] OpenClaw 兼容
│ │ │ └── graph_memory_schema.ts # [NEW] TypeBox Schema
│ │ ├── tool_interface.ts # [UPDATE] 添加 execute 签名
│ │ └── tool_limiter.ts # [NEW] 调用限制器
│ ├── bundled-skills/
│ │ └── graph-memory/ # [RENAMED] kebab-case
│ │ ├── SKILL.md # [FIXED] 单行 frontmatter
│ │ ├── persona/
│ │ │ └── SKILL.md # [FIXED]
│ │ └── task/
│ │ └── SKILL.md # [FIXED]
│ ├── tests/
│ │ └── runtime/core/
│ │ ├── graph_memory/
│ │ │ ├── graph_database.test.ts # [NEW]
│ │ │ └── memory_service.test.ts # [NEW]
│ │ └── tools/builtin/
│ │ └── graph_memory_tool.test.ts # [NEW]
│ ├── package.json # [UPDATE] 添加 openclaw 字段
│ ├── tsconfig.json
│ └── openclaw.plugin.json # [NEW] Plugin Manifest
├── skills/
│ └── graph-memory/ # [RENAMED] kebab-case
│ ├── SKILL.md # [FIXED]
│ ├── persona/
│ │ └── SKILL.md # [FIXED]
│ └── task/
│ └── SKILL.md # [FIXED]
├── docs/integration/waterflow-design.md # [UPDATE]
├── README.md # [UPDATE]
├── README_EN.md # [UPDATE]
├── .gitignore
└── LICENSE
```
---
## 依赖变更
```json
{
"dependencies": {
"yaml": "^2.8.3",
"@sinclair/typebox": "^0.34.0",
"better-sqlite3": "^11.0.0"
},
"devDependencies": {
"@types/node": "^25.5.2",
"@types/better-sqlite3": "^7.6.0",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
}
}
```
---
## 执行顺序与并行策略
```
Phase 1 (P0) ──────────────────────────────────────┐
1.1 依赖 ─→ 1.2 Entry ─→ 1.3 Schema ─→ 1.4 Interface │
├── 必须最先完成
Phase 2 (P0) ──────────────────────────────────────┤ 否则无法在 OpenClaw 中运行
2.1-2.3 SKILL.md 修复(可并行) │
2.4 bundled-skills 同步 │
2.5 目录重命名 │
Phase 3 (P0) ──────────────────────────────────────┤
3.1 SQLite 依赖 │
3.2 GraphDatabase 重写 │
3.3-3.7 功能补全(可部分并行) │
3.8 逻辑修复 │
Phase 4 (P2) ──────────────────────────────────────┤ 优化项,可延后
4.1 ToolLimiter 创建 │
4.2 集成到 Tool │
Phase 5 (P1) ──────────────────────────────────────┘ 在 Phase 1-3 完成后执行
5.1-5.4 测试重建(可并行编写)
Phase 6 (P3) ────────────────────────────────────────── 最后执行,文档收尾
6.1-6.3 文档更新
```
---
## 风险与注意事项
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| better-sqlite3 原生模块编译失败 | 阻塞 Phase 3 | 使用预编译二进制或回退到 sql.js |
| OpenClaw Plugin SDK 版本不兼容 | 阻塞 Phase 1 | 锁定 `compat.pluginApi` 版本 |
| SKILL.md 描述中的中文冒号 | 静默加载失败 | 所有 description 用双引号包裹 |
| SQLite 并发写入冲突 | 数据损坏 | 使用 WAL 模式 + 连接池 |
| depth 遍历性能问题 | 响应缓慢 | 限制最大 depth=5结果上限 100 |
---
## 验收总标准
- [ ] Phase 1-6 全部完成
- [ ] `npm run build` 编译通过,无错误无警告
- [ ] `npm test` 全部通过50+ 用例)
- [ ] 作为 OpenClaw Plugin 可安装、可加载、可调用
- [ ] 数据持久化:写入后重启进程,数据仍然存在
- [ ] 所有 main 分支的核心功能均已实现
- [ ] SKILL.md 通过 OpenClaw 的 `openclaw skills check` 验证

View File

@ -1,95 +0,0 @@
# 重构执行进度追踪
> 最后更新: 2026-04-16 15:00
> 当前分支: openclaw
## Phase 1: 项目结构改造为 OpenClaw Plugin🔴 P0
### 1.1 添加 OpenClaw Plugin SDK 依赖
- [x] 1.1.1 `npm install @sinclair/typebox better-sqlite3`
- [x] 1.1.2 更新 `ts/package.json` 添加 `openclaw` 字段
- [x] 1.1.3 创建 `ts/openclaw.plugin.json` manifest 文件
### 1.2 创建 Plugin Entry Point
- [x] 1.2.1 创建 `ts/src/plugin-entry.ts`
- [x] 1.2.2 确保 entry point 导出为 ESM 格式
- [x] 1.2.3 更新 `ts/tsconfig.json` 确保编译输出路径正确
### 1.3 迁移 Tool Schema 到 TypeBox
- [x] 1.3.1 添加 TypeBox schema 到 plugin-entry.ts
- [x] 1.3.2 更新 `graph_memory_tool.ts``inputSchema` 字段(添加 newRelation, days, dry_run
- [x] 1.3.3 添加 `execute(_id, params)` 方法匹配 OpenClaw 签名
- [x] 1.3.4 返回值格式为 `{ content: [{ type: "text", text }] }`
### 1.4 更新 Tool Interface
- [x] 1.4.1 添加 `OpenClawToolResult` 类型到 graph_memory_tool.ts
## Phase 2: SKILL.md 格式修复(🔴 P0
### 2.1 修复 skills/graph-memory/SKILL.md
- [x] 2.1.1 移除多行嵌套结构
- [x] 2.1.2 name 改为 kebab-case
- [x] 2.1.3 description 改为单行
- [x] 2.1.4 添加 metadata 单行 JSON
### 2.2 修复 skills/graph-memory/persona/SKILL.md
- [x] 2.2.1 同上
### 2.3 修复 skills/graph-memory/task/SKILL.md
- [x] 2.3.1 同上
### 2.4 同步修复 bundled-skills
- [x] 2.4.1 ts/bundled-skills/graph-memory/SKILL.md
- [x] 2.4.2 ts/bundled-skills/graph-memory/persona/SKILL.md
- [x] 2.4.3 ts/bundled-skills/graph-memory/task/SKILL.md
### 2.5 重命名目录
- [x] 2.5.1 skills/graph_memory/ → skills/graph-memory/
- [x] 2.5.2 ts/bundled-skills/graph_memory/ → ts/bundled-skills/graph-memory/
## Phase 3: GraphDatabase 持久化(🔴 P0
### 3.1 添加 SQLite 依赖
- [ ] 3.1.1 npm install better-sqlite3
- [ ] 3.1.2 npm install @types/better-sqlite3
### 3.2 重写 GraphDatabase
- [x] 3.2.1 修改构造函数接受 dbPath
- [x] 3.2.2 使用 better-sqlite3 创建/连接数据库
- [x] 3.2.3 创建实体表
- [x] 3.2.4 创建关系表
- [x] 3.2.5 创建索引
### 3.3-3.7 功能补全
- [x] 3.3 depth 多跳遍历 (BFS)
- [x] 3.4 timeRange 过滤
- [x] 3.5 supersede 模式
- [x] 3.6 archive 归档
- [x] 3.7 cleanup 清理
### 3.8 修复逻辑
- [x] 3.8.1 修复 isEntityDeleted 逻辑 (已移除,用 SQL 替代)
## Phase 4: ToolLimiter🟡 P2
- [ ] 4.1 创建 ToolLimiter
- [ ] 4.2 集成到 GraphMemoryTool
## Phase 5: 测试重建(🔴 P1
- [ ] 5.1 GraphDatabase 测试 (15+)
- [ ] 5.2 MemoryService 测试 (12+)
- [ ] 5.3 GraphMemoryTool 测试 (15+)
- [ ] 5.4 Plugin Entry 集成测试
## Phase 6: 文档更新(🟢 P3
- [ ] 6.1 更新 README.md
- [ ] 6.2 更新 README_EN.md
- [ ] 6.3 更新迁移设计文档
## 最终验收
- [ ] npm run build 编译通过
- [ ] npm test 全部通过
- [ ] 推送到远程 openclaw 分支

View File

@ -1,81 +0,0 @@
# 迁移工作进度追踪
> ⚠️ **修改只在 TrulyMEM 的 waterflow 分支执行** ⚠️
---
## 当前状态
- **开始时间**: 2026-04-15
- **当前任务**: 迁移完成,等待测试
- **最后更新**: 2026-04-15
- **状态**: TypeScript 编译通过
---
## Phase 完成状态
### Phase 1: 项目结构
| 任务 | 状态 | 备注 |
|------|------|------|
| 1.1 ts/ 目录 | ✅ done | |
| 1.2 package.json | ✅ done | |
| 1.3 tsconfig.json | ✅ done | |
### Phase 2: 核心库
| 任务 | 状态 | 备注 |
|------|------|------|
| 2.1 types.ts | ✅ done | |
| 2.2 graph_database.ts | ✅ done | |
| 2.3 memory_service.ts | ✅ done | |
| 2.4 index.ts | ✅ done | |
### Phase 3: Tool 接口
| 任务 | 状态 | 备注 |
|------|------|------|
| 3.1 graph_memory_tool.ts | ✅ done | |
| 3.2 tool_interface.ts | ✅ done | |
### Phase 4: Skill 定义
| 任务 | 状态 | 备注 |
|------|------|------|
| 4.1 SKILL.md (主) | ✅ done | |
| 4.2 persona/SKILL.md | ✅ done | |
| 4.3 task/SKILL.md | ✅ done | |
### Phase 5: 验证
| 任务 | 状态 | 备注 |
|------|------|------|
| 5.1 编译 | ✅ done | TypeScript 编译通过 |
| 5.2 测试 | ⏳ pending | |
---
## 创建的文件
| 文件 | 说明 |
|------|------|
| `ts/package.json` | 项目配置 |
| `ts/tsconfig.json` | TypeScript 配置 |
| `ts/src/runtime/core/graph_memory/types.ts` | 类型定义 |
| `ts/src/runtime/core/graph_memory/graph_database.ts` | 图数据库 |
| `ts/src/runtime/core/graph_memory/memory_service.ts` | 记忆服务 |
| `ts/src/runtime/core/graph_memory/index.ts` | 模块导出 |
| `ts/src/runtime/core/tools/tool_interface.ts` | Tool 接口 |
| `ts/src/runtime/core/tools/builtin/graph_memory_tool.ts` | GraphMemory Tool |
| `ts/bundled-skills/graph_memory/SKILL.md` | 主 Skill |
| `ts/bundled-skills/graph_memory/persona/SKILL.md` | Persona Skill |
| `ts/bundled-skills/graph_memory/task/SKILL.md` | Task Skill |
---
## 说明
- 每次修改文件前后更新此文件
- 记录每次修改的文件和操作
- 方便意外终止后恢复任务

452
README.md
View File

@ -1,178 +1,22 @@
# TrulyMEM - AI 主要长期记忆系统
# TrulyMEM - WaterFlow 适配版
让 AI 拥有真正的长期记忆能力 - OpenClaw 框架插件
让 AI 拥有真正的长期记忆能力 - WaterFlow 框架适配
[English Version](./README_EN.md)
---
## 定位声明
**TrulyMEM 是 AI 的主要长期记忆系统。**
与 OpenClaw 内置的 memory-core 并存运行:
- **memory-core**:负责 session transcripts 和对话历史管理
- **GraphMemory (TrulyMEM)**:负责**结构化长期记忆**——知识图谱、人设、任务、语义搜索
AI 在对话中应**优先**将重要信息(偏好、决策、任务、知识点)写入图数据库,而非依赖 message 历史。
---
## AI 自行安装指南
> 本节面向 AI Agent按步骤指导你自主安装此插件到 OpenClaw 框架。
### 步骤 1克隆仓库
```bash
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
cd TrulyMEM-TrueHumanMEM
git checkout openclaw
```
### 步骤 2编译 TypeScript 插件
```bash
cd ts/
npm install
npm run build
```
编译成功后,`dist/` 目录会输出编译后的 JavaScript 文件。
### 步骤 3安装 Skill让 AI 能调用记忆工具)
`skills/` 目录下的 3 个 Skill 复制到 OpenClaw 的 Skill 目录。优先级:`<workspace>/skills/` > `~/.openclaw/skills/` > `~/.agents/skills/`
```bash
# 方式 A工作空间级推荐仅当前项目可用
cp -r skills/graph-memory <你的项目目录>/skills/graph-memory
cp -r skills/graph-memory-persona <你的项目目录>/skills/graph-memory-persona
cp -r skills/graph-memory-task <你的项目目录>/skills/graph-memory-task
# 方式 B用户级所有项目可用
cp -r skills/graph-memory ~/.openclaw/skills/graph-memory
cp -r skills/graph-memory-persona ~/.openclaw/skills/graph-memory-persona
cp -r skills/graph-memory-task ~/.openclaw/skills/graph-memory-task
```
### 步骤 4安装 Plugin让 OpenClaw 运行时加载 Tool
将编译后的 `ts/` 目录复制到 OpenClaw 的全局扩展目录:
```bash
# 复制到 OpenClaw extensions 目录
mkdir -p ~/.openclaw/extensions
cp -r ts ~/.openclaw/extensions/graph-memory
```
然后启用插件:
```bash
# 启用插件
openclaw plugins enable graph-memory
# 重启 Gateway如果使用 systemd 托管)
systemctl --user restart openclaw-gateway
```
验证插件已加载:
```bash
openclaw plugins list
# 应显示 graph-memory 为 loaded 状态
```
**注意**:插件与 memory-core **并存运行**不会禁用其他内存插件。memory-core 继续管理对话历史GraphMemory 提供结构化长期记忆。
### 步骤 5验证安装
启动 OpenClaw 后,发送以下消息测试:
```
请记住:我喜欢编程,正在学习 TypeScript
```
AI 应调用 `graph_memory` 工具的 `commit` action 并返回成功确认。
然后测试检索:
```
我之前说过喜欢什么?
```
AI 应调用 `recall` action 并返回之前写入的信息。
### 步骤 6确认 Skill 已加载
在 OpenClaw 中执行:
```
/skills
```
你应该能看到以下 3 个 Skill
- `graph-memory` - 记忆 CRUD + 语义搜索 + 上下文压缩
- `graph-memory-persona` - 人设管理
- `graph-memory-task` - 任务管理
---
## 简介
本项目是 OpenClaw 的图记忆插件,基于 SQLite 实现持久化图数据库
本项目是将 TrulyMEM 的图记忆能力迁移到 WaterFlow 框架的 TypeScript 实现
**设计理念AI 的主要长期记忆系统**
本插件作为 AI 的**主要长期记忆存储**,与 memory-core 并存运行:
- **memory-core 管理对话历史**session transcripts 和历史消息由 memory-core 自动管理
- **GraphMemory 管理结构化记忆**:重要事实、人设、任务、知识图谱由 AI 主动写入图数据库
- **AI 优先使用图记忆**对于持久信息AI 应优先写入图数据库而非依赖 message 上下文
**核心功能:**
### 基础记忆操作
- **recall**: 检索记忆(支持关键词、种子实体、多跳遍历、时间过滤)
- **commit**: 写入记忆(三元组批量写入)
- **purge**: 删除记忆(软删除/硬删除/纠错替代)
- **introspect**: 查看记忆状态
- **archive**: 归档旧记忆
- **cleanup**: 清理无效数据
### 高级功能P2
- **memory_search**: 语义搜索——基于本地 ONNX embedding 的向量相似度搜索
- **memory_get**: 精确读取——按路径读取记忆文件内容片段
- **context_rewrite**: 上下文压缩——将长对话历史压缩为关键记忆节点
- **working_memory_chain**: 工作记忆链——获取当前会话的活跃关系链
- **task_node_create/get_recent/get_chain**: 任务节点——创建和追踪连续性任务节点
### 人设与任务管理
- **persona_update/clear**: 人设管理AI 应主动查询人设指导行为)
- **task_create/set_state/delete/link_info**: 任务管理AI 应主动追踪任务状态)
---
## 安装
### 方式一:作为 OpenClaw 插件安装
```bash
cd ts/
npm install
npm run build
```
将插件目录添加到 OpenClaw 配置中,或使用 `openclaw plugins install` 安装。
### 方式二:作为 Skill 安装(推荐)
`skills/` 目录复制到 OpenClaw 的 Skill 目录:
```bash
cp -r skills/graph-memory ~/.agents/skills/graph-memory
cp -r skills/graph-memory-persona ~/.agents/skills/graph-memory-persona
cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
```
作为 WaterFlow 的内置模块,提供图记忆功能:
- **recall**: 检索记忆
- **commit**: 写入记忆
- **purge**: 删除记忆
- **introspect**: 查看状态
- **persona_update/clear**: 人设管理
- **task_create/set_state/delete**: 任务管理
---
@ -180,144 +24,222 @@ cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
```
ts/
├── src/
│ ├── plugin-entry.ts # OpenClaw Plugin 入口
└── runtime/core/
├── graph_memory/
│ ├── types.ts # 类型定义
── graph_database.ts # SQLite 图数据库
│ ├── memory_service.ts # 记忆服务
│ ├── semantic_search.ts # 语义搜索引擎P2
└── index.ts # 模块导出
└── tools/
│ ├── builtin/
│ └── graph_memory_tool.ts # Tool 实现
└── tool_limiter.ts # 调用限制器
├── bundled-skills/
├── graph-memory/ # 内置 Skill 定义
│ └── SKILL.md
│ ├── graph-memory-persona/
└── SKILL.md
│ └── graph-memory-task/
│ └── SKILL.md
├── package.json
├── tsconfig.json
└── openclaw.plugin.json # Plugin Manifest
skills/ # 独立 Skill 定义
├── graph-memory/
│ └── SKILL.md
├── graph-memory-persona/
│ └── SKILL.md
└── graph-memory-task/
└── SKILL.md
├── src/runtime/core/
│ ├── graph_memory/ # 图记忆核心模块
│ ├── types.ts # 类型定义
├── graph_database.ts # 图数据库
│ │ ├── memory_service.ts # 记忆服务
│ │ ── index.ts # 模块导出
└── tools/
└── builtin/
└── graph_memory_tool.ts # Tool 实现
├── bundled-skills/ # Skill 定义
│ └── graph_memory/
├── SKILL.md # 记忆操作
│ ├── persona/SKILL.md # 人设管理
└── task/SKILL.md # 任务管理
├── package.json # 项目配置
└── tsconfig.json # TypeScript 配置
```
---
## 在 OpenClaw 中使用
## 在 WaterFlow 中使用
### 作为 Plugin
本模块完全不动 WaterFlow 源码,只需在你的入口文件中注册即可。
插件入口导出符合 OpenClaw SDK 规范的对象:
### 快速开始(推荐)
#### 步骤 1安装依赖
```bash
npm install /path/to/TrulyMEM-TrueHumanMEM/ts
```
或在 `package.json` 中添加:
```json
{
"dependencies": {
"trulymem-waterflow": "file:../TrulyMEM-TrueHumanMEM/ts"
}
}
```
然后运行:
```bash
npm install
```
#### 步骤 2在你的入口文件中注册
只需两行代码,完全不动 WaterFlow 源码:
```typescript
// plugin-entry.ts 导出格式
export default {
id: 'graph-memory',
name: 'Graph Memory',
description: '让 AI 拥有真正的长期记忆能力',
register(api) {
api.registerTool(tool);
}
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { installTrulyMEM } from 'trulymem/tools';
// 一行安装,返回配置好的 ToolRegistry
const registry = await installTrulyMEM(getPlatform(), 'my-session-id');
// 继续组装 WaterFlow...
const toolExecutor = new ToolExecutor(registry);
```
### 手动注册(更灵活)
如果你想自己控制 ToolRegistry 的创建:
```typescript
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { initializeToolRegistry } from 'waterflow-ts/dist/runtime/core/tools/builtin/index.js';
import { registerGraphMemoryTool } from 'trulymem/tools';
const platform = getPlatform();
const registry = initializeToolRegistry(platform);
// 注册图记忆工具
registerGraphMemoryTool(registry, 'my-session-id');
// 继续组装...
```
### 使用 SkillAI Agent 调用)
#### 步骤 1配置 Skill 来源
```typescript
const config = {
...DEFAULT_SKILL_LOADER_CONFIG,
sources: {
...DEFAULT_SKILL_LOADER_CONFIG.sources,
bundled: './node_modules/trulymem-waterflow/bundled-skills'
},
enabledSources: ['project', 'bundled']
};
```
OpenClaw 加载后会自动调用 `register(api)` 注册工具。
#### 步骤 2通过 Agent 调用
### 作为独立模块
AI Agent 会自动读取 SKILL.md 并调用 `builtin:graph_memory` 工具。
```typescript
import { createGraphMemoryTool } from './dist/runtime/core/tools/builtin/graph_memory_tool.js';
#### 可用 Skill 列表
const tool = createGraphMemoryTool('graph_memory.db', 'my-session-id');
| Skill 名称 | 功能 | 使用场景 |
|------------|------|----------|
| `graph_memory` | 记忆 CRUD | 读取/写入/删除记忆 |
| `persona` | 人设管理 | 设置 AI 角色性格 |
| `task` | 任务管理 | 创建/更新长期任务 |
// 写入记忆
const result = await tool.execute('call-1', {
action: 'commit',
params: {
triplets: [
{ subject: '用户', relation: '喜欢', object: '编程' },
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
]
}
});
#### Skill 定义格式说明
// 语义搜索
const searchResult = await tool.execute('call-2', {
action: 'memory_search',
params: { query: '编程相关', limit: 5 }
});
WaterFlow 的 SkillLoader 会从 `SKILL.md` 中提取:
// 检索记忆
const recallResult = await tool.execute('call-3', {
action: 'recall',
params: { queryIntent: '用户 编程' }
});
```
- **name**: 从目录名提取(如 `graph_memory``persona``task`
- **description**: 从 Markdown 的第一个 `#` 标题提取
- **allowed-tools**: 转换为 `allowedTools` 字段
- **arguments**: 正确映射到 SkillDefinition.arguments
- **user-invocable**: 转换为 `userInvocable` 字段
**注意**: `when_to_use` 信息已整合到 Markdown body 中,通过 SkillRegistry.search() 可匹配。
---
## API
### Actions
### GraphMemoryTool
```typescript
const tool = new GraphMemoryTool(sessionId?: string);
```
#### Actions
| Action | 说明 | 参数 |
|--------|------|------|
| `recall` | 检索记忆 | `queryIntent`, `seedEntities`, `depth`, `sessionFilter`, `timeRange` |
| `recall` | 检索记忆 | `queryIntent`, `seedEntities`, `depth`, `sessionFilter` |
| `commit` | 写入记忆 | `triplets`, `sessionId`, `turnId` |
| `purge` | 删除记忆 | `criteria`, `mode` (soft/hard/supersede), `newRelation` |
| `purge` | 删除记忆 | `criteria`, `mode` |
| `introspect` | 查看状态 | - |
| `archive` | 归档旧记忆 | `days` (默认 30) |
| `cleanup` | 清理无效数据 | `dry_run` (默认 true) |
| **语义搜索** | | |
| `memory_search` | 语义向量搜索 | `query`, `limit`, `corpus` |
| `memory_get` | 精确读取记忆文件 | `path`, `fromLine`, `lines` |
| **上下文压缩** | | |
| `context_rewrite` | 压缩长对话为记忆节点 | `context`, `maxEntities`, `summary` |
| `working_memory_chain` | 获取当前会话活跃关系链 | `maxDepth`, `recentOnly` |
| **任务节点** | | |
| `task_node_create` | 创建任务节点 | `session_id`, `turn_id`, `summary`, `key_facts` |
| `task_node_get_recent` | 获取最近任务节点 | `session_id`, `limit` |
| `task_node_get_chain` | 获取任务链 | `session_id`, `from_node_id` |
| **人设管理** | | |
| `persona_update` | 更新人设 | `attributes`, `mode` (merge/replace) |
| `persona_update` | 更新人设 | `attributes`, `mode` |
| `persona_clear` | 清除人设 | `confirm` |
| **任务管理** | | |
| `task_create` | 创建任务 | `task_id`, `description`, `info_nodes` |
| `task_set_state` | 设置状态 | `task_id`, `state` |
| `task_delete` | 删除任务 | `task_id` |
| `task_link_info` | 关联信息 | `task_id`, `info_node` |
| `task_link_info` | 关联信息到任务 | `task_id`, `info_node` |
---
## Skill 列表
## 示例
| Skill 名称 | 功能 | 使用场景 |
|------------|------|----------|
| `graph-memory` | 记忆 CRUD + 语义搜索 + 上下文压缩 | 读取/写入/删除记忆,语义搜索,压缩历史 |
| `graph-memory-persona` | 人设管理 | 设置 AI 角色性格AI 主动查询人设 |
| `graph-memory-task` | 任务管理 | 创建/更新长期任务AI 主动追踪任务 |
### 写入记忆
```json
{
"action": "commit",
"params": {
"triplets": [
{ "subject": "用户", "relation": "喜欢", "object": "TypeScript" },
{ "subject": "用户", "relation": "正在学习", "object": "WaterFlow" }
]
}
}
```
### 检索记忆
```json
{
"action": "recall",
"params": {
"queryIntent": "用户 学习"
}
}
```
### 创建任务
```json
{
"action": "task_create",
"params": {
"task_id": "Task_学习TypeScript",
"description": "学习 TypeScript 并完成项目",
"info_nodes": ["文档链接", "教程链接"]
}
}
```
### 关联信息到任务
```json
{
"action": "task_link_info",
"params": {
"task_id": "Task_学习TypeScript",
"info_node": "用户喜欢 React"
}
}
```
---
## 开发
## API 名称映射
```bash
cd ts/
npm install
npm run build # 编译
npm test # 运行测试
OpenAI/DeepSeek API 要求工具名称符合 `^[a-zA-Z0-9_-]+$` 格式(不含冒号)。
内部工具 ID 使用 `builtin:xxx` 格式,需映射后发送给 API。
```typescript
import { mapToolIdToApiName, mapApiNameToToolId } from 'trulymem/tools';
// 发送给 API
const apiName = mapToolIdToApiName('builtin:graph_memory'); // -> 'graph_memory'
// 收到 tool_use 后映射回
const internalId = mapApiNameToToolId('graph_memory'); // -> 'builtin:graph_memory'
```
---

View File

@ -1,137 +1,23 @@
# TrulyMEM - OpenClaw Graph Memory Plugin
# TrulyMEM - WaterFlow Adapter
Give AI true long-term memory capability - OpenClaw framework plugin version
Give AI true long-term memory capability - WaterFlow framework adapter version
[中文版本](./README.md)
---
## AI Self-Installation Guide
> This section is for AI Agents. Follow these steps to install this plugin into your OpenClaw framework.
### Step 1: Clone the repository
```bash
git clone https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
cd TrulyMEM-TrueHumanMEM
git checkout openclaw
```
### Step 2: Build the TypeScript plugin
```bash
cd ts/
npm install
npm run build
```
After successful build, the `dist/` directory contains compiled JavaScript files.
### Step 3: Install Skills (enables AI to call memory tools)
Copy the 3 Skills from `skills/` to OpenClaw's skill directory. Priority: `<workspace>/skills/` > `~/.openclaw/skills/` > `~/.agents/skills/`
```bash
# Option A: Workspace-level (recommended, current project only)
cp -r skills/graph-memory <your-project-dir>/skills/graph-memory
cp -r skills/graph-memory-persona <your-project-dir>/skills/graph-memory-persona
cp -r skills/graph-memory-task <your-project-dir>/skills/graph-memory-task
# Option B: User-level (available for all projects)
cp -r skills/graph-memory ~/.openclaw/skills/graph-memory
cp -r skills/graph-memory-persona ~/.openclaw/skills/graph-memory-persona
cp -r skills/graph-memory-task ~/.openclaw/skills/graph-memory-task
```
### Step 4: Install Plugin (enables OpenClaw runtime to load the Tool)
Load the `ts/` directory as an OpenClaw plugin. Add to `openclaw.json`:
```json5
{
"plugins": {
"allow": ["@trulymem/openclaw-graph-memory"]
}
}
```
Or symlink the compiled `ts/dist/` directory to OpenClaw's plugin directory:
```bash
ln -s <path-to-ts/dist> ~/.openclaw/plugins/graph-memory
```
### Step 5: Verify installation
After starting OpenClaw, send this message to test:
```
Please remember: I like programming and I'm learning TypeScript
```
The AI should call the `graph_memory` tool's `commit` action and return a success confirmation.
Then test recall:
```
What did I say I like?
```
The AI should call the `recall` action and return the previously stored information.
### Step 6: Confirm Skills are loaded
In OpenClaw, run:
```
/skills
```
You should see these 3 Skills:
- `graph-memory` - Memory CRUD
- `graph-memory-persona` - Persona management
- `graph-memory-task` - Task management
---
## Introduction
This project is an OpenClaw plugin for graph-based memory with SQLite persistence.
This project ports TrulyMEM's graph memory capability to TypeScript for the WaterFlow framework.
**Core Features:**
- **recall**: Retrieve memories (keyword, seed entities, multi-hop traversal, time filtering)
- **commit**: Write memories (batch triplet writes)
- **purge**: Delete memories (soft/hard/supersede modes)
- **introspect**: View memory statistics
- **archive**: Archive old memories
- **cleanup**: Clean up invalid data
As a built-in module for WaterFlow, it provides graph memory functionality:
- **recall**: Retrieve memories
- **commit**: Commit memories
- **purge**: Delete memories
- **introspect**: Inspect status
- **persona_update/clear**: Persona management
- **task_create/set_state/delete/link_info**: Task management
---
## Installation
### Method 1: As OpenClaw Plugin
```bash
cd ts/
npm install
npm run build
```
Add the plugin directory to your OpenClaw config, or use `openclaw plugins install`.
### Method 2: As Skill (Recommended)
Copy the `skills/` directory to OpenClaw's skill directory:
```bash
cp -r skills/graph-memory ~/.agents/skills/graph-memory
cp -r skills/graph-memory-persona ~/.agents/skills/graph-memory-persona
cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
```
- **task_create/set_state/delete**: Task management
---
@ -139,119 +25,222 @@ cp -r skills/graph-memory-task ~/.agents/skills/graph-memory-task
```
ts/
├── src/
│ ├── plugin-entry.ts # OpenClaw Plugin entry point
└── runtime/core/
├── graph_memory/
│ ├── types.ts # Type definitions
── graph_database.ts # SQLite graph database
│ ├── memory_service.ts # Memory service
│ └── index.ts # Module exports
└── tools/
├── builtin/
└── graph_memory_tool.ts # Tool implementation
└── tool_limiter.ts # Call rate limiter
├── bundled-skills/
├── graph-memory/ # Bundled Skill definitions
└── SKILL.md
├── graph-memory-persona/
└── SKILL.md
│ └── graph-memory-task/
│ └── SKILL.md
├── package.json
├── tsconfig.json
└── openclaw.plugin.json # Plugin Manifest
skills/ # Standalone Skill definitions
├── graph-memory/
│ └── SKILL.md
├── graph-memory-persona/
│ └── SKILL.md
└── graph-memory-task/
└── SKILL.md
├── src/runtime/core/
│ ├── graph_memory/ # Graph memory core module
│ ├── types.ts # Type definitions
├── graph_database.ts # Graph database
│ │ ├── memory_service.ts # Memory service
│ │ ── index.ts # Module exports
└── tools/
└── builtin/
└── graph_memory_tool.ts # Tool implementation
├── bundled-skills/ # Skill definitions
└── graph_memory/
│ ├── SKILL.md # Memory operations
├── persona/SKILL.md # Persona management
└── task/SKILL.md # Task management
├── package.json # Project config
└── tsconfig.json # TypeScript config
```
---
## Usage in OpenClaw
## Usage in WaterFlow
### As Plugin
This module requires **zero changes** to WaterFlow source code. Just register it in your entry file.
```typescript
import registerGraphMemoryPlugin from './dist/plugin-entry.js';
### Quick Start (Recommended)
registerGraphMemoryPlugin({
registerTool(tool) {
// OpenClaw will auto-register the tool
}
});
#### Step 1: Install
```bash
npm install /path/to/TrulyMEM-TrueHumanMEM/ts
```
### As Standalone Module
Or add to `package.json`:
```json
{
"dependencies": {
"trulymem-waterflow": "file:../TrulyMEM-TrueHumanMEM/ts"
}
}
```
Then run:
```bash
npm install
```
#### Step 2: Register in your entry file
Just two lines, zero changes to WaterFlow:
```typescript
import { createGraphMemoryTool } from './dist/runtime/core/tools/builtin/graph_memory_tool.js';
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { installTrulyMEM } from 'trulymem/tools';
const tool = createGraphMemoryTool('graph_memory.db', 'my-session-id');
// One-line install, returns configured ToolRegistry
const registry = await installTrulyMEM(getPlatform(), 'my-session-id');
// Write memory
const result = await tool.execute('call-1', {
action: 'commit',
params: {
triplets: [
{ subject: 'User', relation: 'likes', object: 'programming' },
{ subject: 'User', relation: 'learning', object: 'TypeScript' }
]
}
});
// Recall memory
const recallResult = await tool.execute('call-2', {
action: 'recall',
params: { queryIntent: 'User programming' }
});
// Continue assembling WaterFlow...
const toolExecutor = new ToolExecutor(registry);
```
### Manual Registration (More control)
If you want to control ToolRegistry creation yourself:
```typescript
import { getPlatform } from 'waterflow-ts/dist/platform/index.js';
import { initializeToolRegistry } from 'waterflow-ts/dist/runtime/core/tools/builtin/index.js';
import { registerGraphMemoryTool } from 'trulymem/tools';
const platform = getPlatform();
const registry = initializeToolRegistry(platform);
// Register graph memory tool
registerGraphMemoryTool(registry, 'my-session-id');
// Continue assembling...
```
### Use Skill (AI Agent)
#### Step 1: Configure Skill source
```typescript
const config = {
...DEFAULT_SKILL_LOADER_CONFIG,
sources: {
...DEFAULT_SKILL_LOADER_CONFIG.sources,
bundled: './node_modules/trulymem-waterflow/bundled-skills'
},
enabledSources: ['project', 'bundled']
};
```
#### Step 2: Call via Agent
AI Agent automatically reads SKILL.md and calls `builtin:graph_memory` tool.
#### Available Skills
| Skill Name | Function | Use Case |
|------------|----------|----------|
| `graph_memory` | Memory CRUD | Read/Write/Delete memories |
| `persona` | Persona management | Set AI role/personality |
| `task` | Task management | Create/update long-term tasks |
#### Skill Definition Format
WaterFlow's SkillLoader extracts from `SKILL.md`:
- **name**: Extracted from directory name (e.g., `graph_memory`, `persona`, `task`)
- **description**: Extracted from first Markdown `#` heading
- **allowed-tools**: Converted to `allowedTools` field
- **arguments**: Properly mapped to SkillDefinition.arguments
- **user-invocable**: Converted to `userInvocable` field
**Note**: `when_to_use` info is integrated into Markdown body, searchable via SkillRegistry.search().
---
## API
### Actions
### GraphMemoryTool
```typescript
const tool = new GraphMemoryTool(sessionId?: string);
```
#### Actions
| Action | Description | Parameters |
|--------|-------------|------------|
| `recall` | Retrieve memories | `queryIntent`, `seedEntities`, `depth`, `sessionFilter`, `timeRange` |
| `commit` | Write memories | `triplets`, `sessionId`, `turnId` |
| `purge` | Delete memories | `criteria`, `mode` (soft/hard/supersede), `newRelation` |
| `introspect` | View status | - |
| `archive` | Archive old memories | `days` (default 30) |
| `cleanup` | Clean invalid data | `dry_run` (default true) |
| `persona_update` | Update persona | `attributes`, `mode` (merge/replace) |
| `recall` | Retrieve memories | `queryIntent`, `seedEntities`, `depth`, `sessionFilter` |
| `commit` | Commit memories | `triplets`, `sessionId`, `turnId` |
| `purge` | Delete memories | `criteria`, `mode` |
| `introspect` | Inspect status | - |
| `persona_update` | Update persona | `attributes`, `mode` |
| `persona_clear` | Clear persona | `confirm` |
| `task_create` | Create task | `task_id`, `description`, `info_nodes` |
| `task_set_state` | Set state | `task_id`, `state` |
| `task_delete` | Delete task | `task_id` |
| `task_link_info` | Link info | `task_id`, `info_node` |
| `task_link_info` | Link info to task | `task_id`, `info_node` |
---
## Skills
## Examples
| Skill Name | Function | Use Case |
|------------|----------|----------|
| `graph-memory` | Memory CRUD | Read/write/delete memories |
| `graph-memory-persona` | Persona management | Set AI role/personality |
| `graph-memory-task` | Task management | Create/update long-term tasks |
### Commit Memory
```json
{
"action": "commit",
"params": {
"triplets": [
{ "subject": "User", "relation": "likes", "object": "TypeScript" },
{ "subject": "User", "relation": "is learning", "object": "WaterFlow" }
]
}
}
```
### Recall Memory
```json
{
"action": "recall",
"params": {
"queryIntent": "User learning"
}
}
```
### Create Task
```json
{
"action": "task_create",
"params": {
"task_id": "Task_LearnTypeScript",
"description": "Learn TypeScript and complete project",
"info_nodes": ["Documentation", "Tutorial"]
}
}
```
### Link Info to Task
```json
{
"action": "task_link_info",
"params": {
"task_id": "Task_LearnTypeScript",
"info_node": "User likes React"
}
}
```
---
## Development
## API Name Mapping
```bash
cd ts/
npm install
npm run build # Compile
npm test # Run tests
OpenAI/DeepSeek API requires tool names to match `^[a-zA-Z0-9_-]+$` (no colons).
Internal tool IDs use `builtin:xxx` format and must be mapped before sending to API.
```typescript
import { mapToolIdToApiName, mapApiNameToToolId } from 'trulymem/tools';
// Send to API
const apiName = mapToolIdToApiName('builtin:graph_memory'); // -> 'graph_memory'
// Map back after receiving tool_use
const internalId = mapApiNameToToolId('graph_memory'); // -> 'builtin:graph_memory'
```
---

48
TODO.md
View File

@ -1,48 +0,0 @@
# TrulyMEM 待完成事项
## 项目概述
TrulyMEM - 真正的长期记忆系统 (True Human MEMory)
为 OpenClaw 提供图数据库形式的结构化长期记忆能力。
## 已完成功能
### P0 - 核心功能修复 ✅
- [x] 确认移除 memory 插槽后的插件加载状态
- [x] 测试与 memory-core 并存运行
- [x] 验证工具 schema 正确传递给 Kimi
### P1 - 功能完善 ✅
- [x] 4. 实现完整的工具参数验证
- 为所有 actionrecall/commit/purge/persona_update/persona_clear/task_create/task_set_state/task_delete/task_link_info实现独立验证函数
- 验证规则recall 必需 queryIntent 或 seedEntitiesdepth 1-5commit triplets 非空且字段有效persona_clear 需 confirmtask 需 task_id 和 description 等
- [x] 5. 添加错误处理和日志
- 新增 GraphMemoryLogger 日志系统info/warn/error/action 级别)
- 敏感数据脱敏attributes 只记录属性名triplets 只记录数量
- 参数验证错误返回 validation_error 类型
- 执行错误返回 execution_error 类型
- [x] 6. 完善 skill 文档(说明增强而非替换)
- 更新 3 个 skill 文档graph-memory、graph-memory-persona、graph-memory-task
- 明确说明是 OpenClaw memory-core 的增强补充,不替代核心功能
- 添加与 memory-core 的关系对比表
## 进行中 / 待完成
### P2 - 可选高级功能
- [ ] 7. 实现 context_rewrite 工具(压缩上下文)
- [ ] 8. 实现工作记忆链机制
- [ ] 9. 实现人设强制查询(作为 skill 而非核心)
## 技术规格
### 测试覆盖
- 测试文件:`ts/tests/runtime/core/tools/builtin/graph_memory_tool.test.ts`
- 当前测试数:**118 个全部通过**
- 参数验证测试11 个(覆盖所有 action 的必填参数、范围校验等)
### 提交记录
- 最新提交:`P1: 完整参数验证 + 错误处理/日志 + 测试覆盖`
## 注意事项
- 所有功能均作为 OpenClaw 插件实现,不修改 OpenClaw 核心代码
- 插件入口:`ts/src/plugin-entry.ts`
- 技能目录:`skills/`(源文件)和 `ts/bundled-skills/`(编译后)

View File

@ -1,811 +0,0 @@
# TrulyMEM → WaterFlow 迁移设计文档
**版本**: 1.0
**日期**: 2026-04-15
**目标**: 用 TypeScript 完全重写 TrulyMEM 的图记忆能力,集成到 WaterFlow
---
## 一、迁移策略
### 1.1 核心原则
- **完全重写**: 不保留 Python 代码,用 TypeScript 实现
- **架构一致**: 遵循 WaterFlow 的架构风格和设计模式
- **原生集成**: 作为 WaterFlow 的内置模块,而非外部依赖
### 1.2 迁移范围
| TrulyMEM (Python) | WaterFlow (TypeScript) | 说明 |
|-------------------|------------------------|------|
| `EmbeddedGraphDB` | `GraphDatabase` | SQLite 图数据库重写 |
| `GraphMemoryClient` | `MemoryService` | 记忆服务 |
| 12 个记忆工具 | `GraphMemoryTool` | WaterFlow Tool 接口 |
| System Prompt | 提示词模板 | 提示词管理 |
| TUI | ❌ 不迁移 | WaterFlow 无 TUI |
---
## 二、架构设计
### 2.1 整体架构
```
┌─────────────────────────────────────────────────────────────────────┐
│ WaterFlow Core │
│ ┌─────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Agent / │───>│ Query │───>│ ToolExecutor │ │
│ │ Workflow │ │ Engine │ │ │ │
│ └─────────────┘ └──────────────┘ └───────────┬────────────┘ │
│ │ │
│ ┌──────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ GraphMemory Module (NEW) │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ GraphMemoryTool │ │ GraphDatabase │ │ MemoryService │ │ │
│ │ │ (Tool Interface)│ │ (SQLite Graph) │ │ (LLM Integration)│ │ │
│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │
│ │ │ │ │ │ │
│ │ └────────────────────┼────────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ GraphMemoryStore │ │ │
│ │ │ (In-Memory Cache) │ │ │
│ │ └─────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
```
### 2.2 模块职责
| 模块 | 职责 | 位置 |
|------|------|------|
| `GraphMemoryTool` | WaterFlow Tool 接口,暴露记忆能力 | `runtime/core/tools/builtin/graph_memory/` |
| `GraphDatabase` | SQLite 图数据库实现 | `runtime/core/graph_memory/database/` |
| `MemoryService` | 封装业务逻辑 | `runtime/core/graph_memory/service/` |
| `GraphMemoryStore` | 内存缓存,加速查询 | `runtime/core/graph_memory/store/` |
| `SystemPrompt` | 提示词模板管理 | `runtime/core/graph_memory/prompts/` |
---
## 三、目录结构
### 3.1 新增目录
```
WaterFlow/ts/src/
├── runtime/core/
│ ├── graph_memory/ # 新增: 图记忆模块
│ │ ├── index.ts # 模块导出
│ │ ├── types.ts # 类型定义
│ │ ├── database/ # 图数据库实现
│ │ │ ├── index.ts
│ │ │ ├── graph_database.ts # 主类
│ │ │ ├── entity_store.ts # 实体存储
│ │ │ └── relation_store.ts # 关系存储
│ │ ├── service/ # 服务层
│ │ │ ├── index.ts
│ │ │ ├── memory_service.ts # 记忆服务
│ │ │ ├── recall_service.ts # 检索服务
│ │ │ └── task_service.ts # 任务服务
│ │ ├── store/ # 缓存层
│ │ │ ├── index.ts
│ │ │ └── memory_cache.ts
│ │ └── prompts/ # 提示词
│ │ └── system_prompt.ts
│ │
│ └── tools/builtin/
│ └── graph_memory/ # GraphMemory Tool
│ ├── index.ts
│ ├── graph_memory_tool.ts # Tool 实现
│ ├── types.ts # Tool 参数类型
│ └── tool_registry.ts # 自动注册
```
### 3.2 修改文件
| 文件 | 修改内容 |
|------|----------|
| `runtime/core/tools/builtin/index.ts` | 注册 GraphMemoryTool |
| `shared/types/index.ts` | 导出图记忆类型 |
---
## 四、核心类型定义
### 4.1 图数据库类型
```typescript
// src/runtime/core/graph_memory/types.ts
export interface Entity {
id: string;
name: string;
type: string;
mentionCount: number;
createdAt: Date;
updatedAt: Date;
}
export interface Relation {
id: string;
sourceId: string;
targetId: string;
relationType: string;
confidence: number;
status: RelationStatus;
sessionId: string;
turnId: number;
createdAt: Date;
updatedAt: Date;
dateBucket: string;
}
export type RelationStatus = 'active' | 'deleted' | 'archived' | 'superseded';
export interface Triplet {
subject: string;
relation: string;
object: string;
confidence?: number;
}
export interface RecallParams {
queryIntent: string;
seedEntities?: string[];
depth?: number;
timeRange?: { days: number };
sessionFilter?: string;
}
export interface CommitParams {
triplets: Triplet[];
entityTypes?: Record<string, string>;
temporalTag?: string;
sessionId?: string;
turnId?: number;
}
export interface PurgeParams {
criteria: {
subject?: string;
target?: string;
relation?: string;
sessionId?: string;
};
mode?: 'soft' | 'hard' | 'supersede';
newRelation?: { relation: string; target: string };
}
export type TaskState = '进行中' | '已完成' | '已暂停' | '已取消';
export interface MemoryStats {
entityCount: number;
relationCount: number;
sessionId?: string;
}
```
### 4.2 Tool 参数类型
```typescript
// src/runtime/core/tools/builtin/graph_memory/types.ts
export type GraphMemoryAction =
| 'recall' | 'commit' | 'purge' | 'introspect' | 'archive' | 'cleanup'
| 'persona_update' | 'persona_clear'
| 'task_create' | 'task_set_state' | 'task_delete' | 'task_link_info';
export interface GraphMemoryToolInput {
action: GraphMemoryAction;
params: Record<string, unknown>;
}
```
---
## 五、核心实现
### 5.1 GraphDatabase 实现
```typescript
// src/runtime/core/graph_memory/database/graph_database.ts
export class GraphDatabase {
private db: Database;
constructor(dbPath: string) {
this.db = new Database(dbPath);
this.initialize();
}
private initialize(): void {
this.db.exec(`
CREATE TABLE IF NOT EXISTS entities (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
type TEXT,
mention_count INTEGER DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
this.db.exec(`
CREATE TABLE IF NOT EXISTS relations (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
target_id TEXT NOT NULL,
relation_type TEXT NOT NULL,
confidence REAL DEFAULT 1.0,
status TEXT DEFAULT 'active',
session_id TEXT,
turn_id INTEGER,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
date_bucket TEXT
)
`);
// 索引
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name);
CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status);
`);
}
async recall(params: RecallParams): Promise<RecallResult> {
const { queryIntent, seedEntities, sessionFilter } = params;
const keywords = queryIntent.split(/[,\s]+/).filter(k => k.length > 0);
const entities: Entity[] = [];
const relations: Relation[] = [];
const entityIds = new Set<string>();
// 搜索实体
for (const keyword of keywords) {
const rows = this.db.exec(
`SELECT * FROM entities WHERE LOWER(name) LIKE ? LIMIT 50`,
[`%${keyword.toLowerCase()}%`]
);
for (const row of rows) {
if (!entityIds.has(row.id)) {
entityIds.add(row.id);
entities.push(this.rowToEntity(row));
}
}
}
// 搜索关系
if (entityIds.size > 0) {
const placeholders = Array.from(entityIds).map(() => '?').join(',');
let query = `
SELECT r.*, e1.name as source_name, e2.name as target_name
FROM relations r
JOIN entities e1 ON r.source_id = e1.id
JOIN entities e2 ON r.target_id = e2.id
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
AND r.status = 'active'
`;
const queryParams = [...entityIds, ...entityIds];
if (sessionFilter) {
query += ` AND r.session_id = ?`;
queryParams.push(sessionFilter);
}
const rows = this.db.exec(query, queryParams);
for (const row of rows) {
relations.push(this.rowToRelation(row));
}
}
return { entities, relations, message: `找到 ${entities.length} 个实体, ${relations.length} 条关系` };
}
async commit(params: CommitParams): Promise<{ createdEntities: number; createdRelations: number }> {
const { triplets, sessionId, turnId } = params;
let createdEntities = 0;
let createdRelations = 0;
for (const triplet of triplets) {
const sourceId = this.upsertEntity(triplet.subject);
const targetId = this.upsertEntity(triplet.object);
this.db.exec(`
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status)
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
`, [this.generateId(), sourceId, targetId, triplet.relation, triplet.confidence || 1.0, sessionId, turnId || 0]);
createdEntities += 2;
createdRelations++;
}
return { createdEntities, createdRelations };
}
async purge(params: PurgeParams): Promise<{ deleted: number; mode: string }> {
const { criteria, mode = 'soft' } = params;
const conditions: string[] = ['status = ?'];
const values: unknown[] = ['active'];
if (criteria.subject) {
conditions.push(`source_id IN (SELECT id FROM entities WHERE name = ?)`);
values.push(criteria.subject);
}
const whereClause = conditions.join(' AND ');
const result = this.db.exec(`UPDATE relations SET status = 'deleted' WHERE ${whereClause}`, values);
return { deleted: result.length, mode };
}
async introspect(): Promise<MemoryStats> {
const entityCount = this.db.exec(`SELECT COUNT(*) as c FROM entities`)[0]?.c || 0;
const relationCount = this.db.exec(`SELECT COUNT(*) as c FROM relations WHERE status = 'active'`)[0]?.c || 0;
return { entityCount, relationCount };
}
private upsertEntity(name: string): string {
const existing = this.db.exec(`SELECT id FROM entities WHERE name = ?`, [name]);
if (existing.length > 0) {
this.db.exec(`UPDATE entities SET mention_count = mention_count + 1 WHERE name = ?`, [name]);
return existing[0].id;
}
const id = this.generateId();
this.db.exec(`INSERT INTO entities (id, name, type) VALUES (?, ?, ?)`, [id, name, 'unknown']);
return id;
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
private rowToEntity(row: any): Entity {
return {
id: row.id, name: row.name, type: row.type || 'unknown',
mentionCount: row.mention_count || 1,
createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at)
};
}
private rowToRelation(row: any): Relation {
return {
id: row.id, sourceId: row.source_id, targetId: row.target_id,
relationType: row.relation_type, confidence: row.confidence || 1.0,
status: row.status || 'active', sessionId: row.session_id || '',
turnId: row.turn_id || 0,
createdAt: new Date(row.created_at), updatedAt: new Date(row.updated_at),
dateBucket: row.date_bucket || ''
};
}
close(): void { this.db.close(); }
}
```
### 5.2 GraphMemoryTool 实现
```typescript
// src/runtime/core/tools/builtin/graph_memory/graph_memory_tool.ts
import type { Tool, ToolExecutionContext, ToolInputSchema } from '../tool_interface';
import { GraphDatabase } from '../../../graph_memory/database/graph_database';
import { MemoryService } from '../../../graph_memory/service/memory_service';
export class GraphMemoryTool implements Tool {
readonly id = 'builtin:graph_memory';
readonly name = 'GraphMemory';
readonly description = `图记忆工具 - 让 AI 拥有真正的长期记忆能力
操作:
- recall: 检索记忆
- commit: 写入记忆
- purge: 删除记忆
- introspect: 查看状态
- persona_update/clear: 人设管理
- task_create/set_state/delete: 任务管理`;
readonly category = 'analysis';
readonly permissionLevel: 'safe' = 'safe';
readonly inputSchema: ToolInputSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['recall', 'commit', 'purge', 'introspect', 'persona_update', 'persona_clear',
'task_create', 'task_set_state', 'task_delete', 'task_link_info'],
description: '记忆操作类型'
},
params: { type: 'object', description: '操作参数' }
},
required: ['action', 'params']
};
private db: GraphDatabase;
private service: MemoryService;
constructor(config: { dbPath: string; sessionId?: string }) {
this.db = new GraphDatabase(config.dbPath);
this.service = new MemoryService(this.db, config.sessionId);
}
async handler(params: Record<string, unknown>, context: ToolExecutionContext): Promise<string> {
const action = params.action as string;
const actionParams = params.params as Record<string, unknown>;
try {
const result = await this.executeAction(action, actionParams);
return JSON.stringify({ success: true, data: result }, null, 2);
} catch (error) {
return JSON.stringify({
success: false,
error: { type: 'execution_error', message: error instanceof Error ? error.message : String(error) }
}, null, 2);
}
}
private async executeAction(action: string, params: Record<string, unknown>): Promise<unknown> {
switch (action) {
case 'recall': return this.service.recall(params as any);
case 'commit': return this.service.commit(params as any);
case 'purge': return this.service.purge(params as any);
case 'introspect': return this.service.introspect();
case 'persona_update': return this.service.updatePersona(params);
case 'persona_clear': return this.service.clearPersona(params);
case 'task_create': return this.service.createTask(params);
case 'task_set_state': return this.service.setTaskState(params);
case 'task_delete': return this.service.deleteTask(params);
default: throw new Error(`Unknown action: ${action}`);
}
}
close(): void { this.db.close(); }
}
```
### 5.3 MemoryService 实现
```typescript
// src/runtime/core/graph_memory/service/memory_service.ts
import { GraphDatabase } from '../database/graph_database';
import type { RecallParams, CommitParams, PurgeParams } from '../types';
export class MemoryService {
private db: GraphDatabase;
private sessionId: string;
constructor(db: GraphDatabase, sessionId?: string) {
this.db = db;
this.sessionId = sessionId || `session-${Date.now()}`;
}
async recall(params: RecallParams) {
return this.db.recall({ ...params, sessionFilter: params.sessionFilter || this.sessionId });
}
async commit(params: CommitParams) {
return this.db.commit({ ...params, sessionId: params.sessionId || this.sessionId });
}
async purge(params: PurgeParams) {
return this.db.purge(params);
}
async introspect() {
const stats = await this.db.introspect();
return { ...stats, sessionId: this.sessionId };
}
async updatePersona(params: Record<string, unknown>) {
const attributes = params.attributes as Array<{ attribute: string; value: string }>;
const mode = params.mode as string || 'merge';
if (mode === 'replace') {
await this.db.purge({ criteria: { subject: 'AI' }, mode: 'soft' });
}
const triplets = attributes.map(attr => ({
subject: 'AI', relation: attr.attribute, object: attr.value, confidence: 1.0
}));
await this.commit({ triplets });
return { status: 'success', updatedAttributes: attributes.length };
}
async clearPersona(params: Record<string, unknown>) {
if (params.confirm === false) return { status: 'cancelled', deletedCount: 0 };
const result = await this.purge({ criteria: { subject: 'AI' }, mode: 'soft' });
return { status: 'success', deletedCount: result.deleted };
}
async createTask(params: Record<string, unknown>) {
const taskId = params.task_id as string;
const description = params.description as string;
const infoNodes = (params.info_nodes as string[]) || [];
await this.commit({
triplets: [
{ subject: taskId, relation: 'is_type', object: 'TaskNode' },
{ subject: taskId, relation: 'has_description', object: description },
{ subject: taskId, relation: 'HAS_STATE', object: 'State_进行中' }
]
});
if (infoNodes.length > 0) {
await this.commit({
triplets: infoNodes.map(node => ({ subject: taskId, relation: 'CONTAINS_INFO', object: node }))
});
}
return { status: 'success', taskId };
}
async setTaskState(params: Record<string, unknown>) {
const taskId = params.task_id as string;
const state = params.state as string;
await this.purge({ criteria: { subject: taskId, relation: 'HAS_STATE' }, mode: 'soft' });
await this.commit({ triplets: [{ subject: taskId, relation: 'HAS_STATE', object: `State_${state}` }] });
return { status: 'success', newState: state };
}
async deleteTask(params: Record<string, unknown>) {
const taskId = params.task_id as string;
await this.purge({ criteria: { subject: taskId }, mode: 'soft' });
return { status: 'success', taskId };
}
}
```
---
## 六、测试方案
### 6.1 测试文件结构
```
WaterFlow/ts/tests/
├── runtime/core/graph_memory/
│ ├── database/
│ │ └── graph_database.test.ts # 15+ 测试
│ └── service/
│ └── memory_service.test.ts # 12+ 测试
└── runtime/core/tools/builtin/
└── graph_memory/
└── graph_memory_tool.test.ts # 15+ 测试
```
### 6.2 数据库测试
```typescript
// tests/runtime/core/graph_memory/database/graph_database.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GraphDatabase } from '../../../../../src/runtime/core/graph_memory/database/graph_database';
import * as fs from 'fs';
describe('GraphDatabase', () => {
const testDbPath = '/tmp/test_graph_memory.db';
let db: GraphDatabase;
beforeEach(() => {
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
db = new GraphDatabase(testDbPath);
});
afterEach(() => {
db.close();
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
});
describe('commit', () => {
it('should create entities and relations', async () => {
const result = await db.commit({
triplets: [
{ subject: '用户', relation: '喜欢', object: 'Python' },
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
]
});
expect(result.createdEntities).toBe(3);
expect(result.createdRelations).toBe(2);
});
});
describe('recall', () => {
beforeEach(async () => {
await db.commit({
triplets: [
{ subject: '用户', relation: '喜欢', object: 'Python' },
{ subject: 'Python', relation: '是', object: '编程语言' }
]
});
});
it('should recall by keyword', async () => {
const result = await db.recall({ queryIntent: 'Python' });
expect(result.entities.some(e => e.name === 'Python')).toBe(true);
});
it('should recall relations', async () => {
const result = await db.recall({ queryIntent: '用户,Python' });
expect(result.relations.length).toBeGreaterThan(0);
});
});
describe('purge', () => {
beforeEach(async () => {
await db.commit({
triplets: [{ subject: '旧信息', relation: 'is', object: '垃圾' }]
});
});
it('should soft delete relations', async () => {
const result = await db.purge({ criteria: { subject: '旧信息' }, mode: 'soft' });
expect(result.deleted).toBeGreaterThan(0);
expect(result.mode).toBe('soft');
});
});
describe('introspect', () => {
it('should return statistics', async () => {
await db.commit({ triplets: [{ subject: 'A', relation: 'relates', object: 'B' }] });
const stats = await db.introspect();
expect(stats.entityCount).toBe(2);
expect(stats.relationCount).toBe(1);
});
});
});
```
### 6.3 服务层测试
```typescript
// tests/runtime/core/graph_memory/service/memory_service.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { MemoryService } from '../../../../../src/runtime/core/graph_memory/service/memory_service';
import { GraphDatabase } from '../../../../../src/runtime/core/graph_memory/database/graph_database';
describe('MemoryService', () => {
const testDbPath = '/tmp/test_memory_service.db';
let db: GraphDatabase;
let service: MemoryService;
beforeEach(() => {
db = new GraphDatabase(testDbPath);
service = new MemoryService(db, 'test-session');
});
describe('persona management', () => {
it('should update persona', async () => {
const result = await service.updatePersona({
attributes: [{ attribute: '角色', value: '猫娘' }],
mode: 'replace'
});
expect(result.status).toBe('success');
expect(result.updatedAttributes).toBe(1);
});
it('should clear persona', async () => {
await service.updatePersona({ attributes: [{ attribute: '角色', value: '猫娘' }] });
const result = await service.clearPersona({ confirm: true });
expect(result.status).toBe('success');
});
});
describe('task management', () => {
it('should create task', async () => {
const result = await service.createTask({
task_id: 'Task_Test',
description: '测试任务',
info_nodes: ['info1']
});
expect(result.taskId).toBe('Task_Test');
});
it('should set task state', async () => {
await service.createTask({ task_id: 'Task_State', description: '测试' });
const result = await service.setTaskState({ task_id: 'Task_State', state: '已完成' });
expect(result.newState).toBe('已完成');
});
});
});
```
### 6.4 Tool 接口测试
```typescript
// tests/runtime/core/tools/builtin/graph_memory/graph_memory_tool.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { GraphMemoryTool } from '../../../../../src/runtime/core/tools/builtin/graph_memory/graph_memory_tool';
import * as fs from 'fs';
describe('GraphMemoryTool', () => {
const testDbPath = '/tmp/test_graph_memory_tool.db';
let tool: GraphMemoryTool;
beforeEach(() => {
if (fs.existsSync(testDbPath)) fs.unlinkSync(testDbPath);
tool = new GraphMemoryTool({ dbPath: testDbPath, sessionId: 'test' });
});
it('should have correct metadata', () => {
expect(tool.id).toBe('builtin:graph_memory');
expect(tool.name).toBe('GraphMemory');
expect(tool.category).toBe('analysis');
});
describe('recall', () => {
it('should execute recall', async () => {
await tool.handler({ action: 'commit', params: { triplets: [{ subject: 'Test', relation: 't', object: 'D' }] } }, mockContext());
const result = await tool.handler({ action: 'recall', params: { query_intent: 'Test' } }, mockContext());
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
});
});
describe('commit', () => {
it('should execute commit', async () => {
const result = await tool.handler({
action: 'commit',
params: { triplets: [{ subject: '用户', relation: '喜欢', object: 'AI' }] }
}, mockContext());
const parsed = JSON.parse(result);
expect(parsed.success).toBe(true);
});
});
describe('error handling', () => {
it('should return error for unknown action', async () => {
const result = await tool.handler({ action: 'unknown', params: {} }, mockContext());
const parsed = JSON.parse(result);
expect(parsed.success).toBe(false);
});
});
});
function mockContext() {
return {
toolCallId: 'test', workingDirectory: '/tmp', abortController: { signal: {} },
config: { timeout: 5000 }, logger: { info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }
};
}
```
### 6.5 验证检查清单
```
[ ] GraphDatabase.commit - 创建实体和关系
[ ] GraphDatabase.recall - 按关键词检索
[ ] GraphDatabase.purge - 软删除
[ ] GraphDatabase.introspect - 返回统计
[ ] MemoryService.updatePersona - 人设更新
[ ] MemoryService.clearPersona - 人设清除
[ ] MemoryService.createTask - 创建任务
[ ] MemoryService.setTaskState - 设置状态
[ ] MemoryService.deleteTask - 删除任务
[ ] GraphMemoryTool recall action
[ ] GraphMemoryTool commit action
[ ] GraphMemoryTool persona_update action
[ ] GraphMemoryTool task_create action
[ ] GraphMemoryTool error handling
```
---
## 七、实现计划
| Phase | 任务 | 周期 | 测试 |
|-------|------|------|------|
| 1 | GraphDatabase 实现 | 2-3 天 | 15+ |
| 2 | MemoryService 实现 | 1-2 天 | 12+ |
| 3 | GraphMemoryTool 实现 | 1-2 天 | 15+ |
| 4 | 集成测试 | 1 天 | 8+ |
**总计**: 5-8 天50+ 测试用例

897
package-lock.json generated
View File

@ -1,897 +0,0 @@
{
"name": "TrulyMEM-TrueHumanMEM",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@xenova/transformers": "^2.17.2"
}
},
"node_modules/@huggingface/jinja": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz",
"integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@protobufjs/aspromise": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/base64": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/codegen": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/eventemitter": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/fetch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.1",
"@protobufjs/inquire": "^1.1.0"
}
},
"node_modules/@protobufjs/float": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/inquire": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/path": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/pool": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
"license": "BSD-3-Clause"
},
"node_modules/@protobufjs/utf8": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
"integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==",
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.19.0"
}
},
"node_modules/@xenova/transformers": {
"version": "2.17.2",
"resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz",
"integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==",
"license": "Apache-2.0",
"dependencies": {
"@huggingface/jinja": "^0.2.2",
"onnxruntime-web": "1.14.0",
"sharp": "^0.32.0"
},
"optionalDependencies": {
"onnxruntime-node": "1.14.0"
}
},
"node_modules/b4a": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz",
"integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==",
"license": "Apache-2.0",
"peerDependencies": {
"react-native-b4a": "*"
},
"peerDependenciesMeta": {
"react-native-b4a": {
"optional": true
}
}
},
"node_modules/bare-events": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
"license": "Apache-2.0",
"peerDependencies": {
"bare-abort-controller": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
}
}
},
"node_modules/bare-fs": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
"integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4",
"bare-url": "^2.2.2",
"fast-fifo": "^1.3.2"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz",
"integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz",
"integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/bare-url": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz",
"integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"license": "ISC"
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/deep-extend": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"license": "MIT",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/events-universal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
"integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.7.0"
}
},
"node_modules/expand-template": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-fifo": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
"license": "MIT"
},
"node_modules/flatbuffers": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz",
"integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==",
"license": "SEE LICENSE IN LICENSE.txt"
},
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"license": "MIT"
},
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"license": "MIT"
},
"node_modules/guid-typescript": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz",
"integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==",
"license": "ISC"
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"license": "ISC"
},
"node_modules/is-arrayish": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
"license": "MIT"
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
"license": "Apache-2.0"
},
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"license": "MIT"
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
"license": "MIT"
},
"node_modules/node-abi": {
"version": "3.89.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz",
"integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
"license": "MIT"
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/onnx-proto": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz",
"integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==",
"license": "MIT",
"dependencies": {
"protobufjs": "^6.8.8"
}
},
"node_modules/onnxruntime-common": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz",
"integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==",
"license": "MIT"
},
"node_modules/onnxruntime-node": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz",
"integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==",
"license": "MIT",
"optional": true,
"os": [
"win32",
"darwin",
"linux"
],
"dependencies": {
"onnxruntime-common": "~1.14.0"
}
},
"node_modules/onnxruntime-web": {
"version": "1.14.0",
"resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz",
"integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==",
"license": "MIT",
"dependencies": {
"flatbuffers": "^1.12.0",
"guid-typescript": "^1.0.9",
"long": "^4.0.0",
"onnx-proto": "^4.0.4",
"onnxruntime-common": "~1.14.0",
"platform": "^1.3.6"
}
},
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==",
"license": "MIT"
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.",
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^2.0.0",
"node-abi": "^3.3.0",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"bin": {
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prebuild-install/node_modules/tar-fs": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz",
"integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.1.4"
}
},
"node_modules/prebuild-install/node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/protobufjs": {
"version": "6.11.5",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.5.tgz",
"integrity": "sha512-OKjVH3hDoXdIZ/s5MLv8O2X0s+wOxGfV7ar6WFSKGaSAxi/6gYn3px5POS4vi+mc/0zCOdL7Jkwrj0oT1Yst2A==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
"@protobufjs/codegen": "^2.0.4",
"@protobufjs/eventemitter": "^1.1.0",
"@protobufjs/fetch": "^1.1.0",
"@protobufjs/float": "^1.0.2",
"@protobufjs/inquire": "^1.1.0",
"@protobufjs/path": "^1.1.2",
"@protobufjs/pool": "^1.1.0",
"@protobufjs/utf8": "^1.1.0",
"@types/long": "^4.0.1",
"@types/node": ">=13.7.0",
"long": "^4.0.0"
},
"bin": {
"pbjs": "bin/pbjs",
"pbts": "bin/pbts"
}
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.2",
"node-addon-api": "^6.1.0",
"prebuild-install": "^7.1.1",
"semver": "^7.5.4",
"simple-get": "^4.0.1",
"tar-fs": "^3.0.4",
"tunnel-agent": "^0.6.0"
},
"engines": {
"node": ">=14.15.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"decompress-response": "^6.0.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/streamx": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
"fast-fifo": "^1.3.2",
"text-decoder": "^1.1.0"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/tar-fs": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz",
"integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==",
"license": "MIT",
"dependencies": {
"pump": "^3.0.0",
"tar-stream": "^3.1.5"
},
"optionalDependencies": {
"bare-fs": "^4.0.1",
"bare-path": "^3.0.0"
}
},
"node_modules/tar-stream": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz",
"integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==",
"license": "MIT",
"dependencies": {
"b4a": "^1.6.4",
"bare-fs": "^4.5.5",
"fast-fifo": "^1.2.0",
"streamx": "^2.15.0"
}
},
"node_modules/teex": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
"integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
"license": "MIT",
"dependencies": {
"streamx": "^2.12.5"
}
},
"node_modules/text-decoder": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
"integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
"license": "Apache-2.0",
"dependencies": {
"b4a": "^1.6.4"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
},
"engines": {
"node": "*"
}
},
"node_modules/undici-types": {
"version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
}
}
}

View File

@ -1,5 +0,0 @@
{
"dependencies": {
"@xenova/transformers": "^2.17.2"
}
}

View File

@ -1,103 +0,0 @@
---
name: graph-memory-persona-force
description: "AI 人设强制查询最佳实践 - 指导 AI 在特定场景下主动查询人设图"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: false
---
# Graph Memory Persona Force - 人设强制查询 Skill
本 Skill 不是修改 OpenClaw 核心,而是作为**最佳实践指导**,帮助 AI 在合适的时机主动查询用户的长期记忆(人设图)。
## 何时查询人设?
当对话中出现以下信号时AI **应当主动调用 graph_memory recall** 查询用户相关记忆:
### 1. 个人偏好信号
- "我喜欢..." / "我不喜欢..."
- "我更倾向于..." / "我讨厌..."
- "我习惯..." / "我总是..."
- **行动**recall 查询 "偏好" 相关记忆
### 2. 重要决策信号
- "我决定..." / "我选了..."
- "我打算..." / "我准备..."
- "我确定用..." / "我最终选择..."
- **行动**recall 查询 "决策" 相关记忆commit 记录新决策
### 3. 目标/计划信号
- "我的目标是..." / "我想实现..."
- "我计划..." / "我希望..."
- **行动**recall 查询 "目标" 相关记忆
### 4. 问题/困难信号
- "我遇到一个问题..." / "我不确定..."
- "我尝试了...但失败了" / "有什么建议..."
- **行动**recall 查询历史 "解决方案",看是否有类似经历
### 5. 情绪/状态信号
- "我最近..." / "我感觉..."
- "我很忙..." / "我没时间..."
- **行动**recall 查询 "状态" 或 "情绪" 相关记忆
## 查询策略
```json
{
"action": "recall",
"params": {
"queryIntent": "用户偏好 决策",
"seedEntities": ["用户", "我"],
"depth": 2
}
}
```
### 渐进式查询
1. **先查人设核心** (subject="AI" 或 "用户")
2. **再查相关实体** (seedEntities 包含关键词)
3. **最后查上下文** (working_memory_chain)
## 记录时机
当用户明确表达新的偏好、决策或目标时,**立即 commit**
```json
{
"action": "commit",
"params": {
"triplets": [
{"subject": "用户", "relation": "偏好", "object": "Python"},
{"subject": "用户", "relation": "决策", "object": "选择React作为前端框架"}
]
}
}
```
## 示例对话
**用户**:我最近在学习 TypeScript因为之前用 JavaScript 遇到太多类型问题了。
**AI 思考**
1. 用户提到 "学习 TypeScript" → 可能是新偏好/目标
2. 用户提到 "之前用 JavaScript 遇到类型问题" → 历史决策原因
3. **行动**recall 查询用户技术偏好
```json
{
"action": "recall",
"params": {
"queryIntent": "技术偏好 JavaScript TypeScript",
"seedEntities": ["用户", "JavaScript", "TypeScript"],
"depth": 2
}
}
```
## 与 graph-memory Skill 的关系
- `graph-memory`提供工具能力recall/commit/purge 等)
- `graph-memory-persona-force`:指导何时使用、如何使用(最佳实践)
两者配合使用,实现真正智能的长期记忆系统。

View File

@ -1,82 +0,0 @@
---
name: graph-memory-persona
description: "管理 AI 人设 - AI 应主动查询和维护角色特征,将人设信息作为长期记忆存储在图数据库中"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# GraphMemory Persona 人设管理
管理 AI 的人设/角色特征。
> **定位声明**:人设是 AI 长期记忆的一部分存储在图数据库中。AI **应主动查询**人设信息来指导自己的行为和回应风格。
## 核心原则
**AI 必须在以下场景主动查询人设**
1. 对话开始时(检索用户偏好和 AI 角色设定)
2. 用户要求改变行为或风格时
3. 不确定如何回应时(查询是否有相关人设指导)
4. 用户提到"你之前说..."、"我告诉过你..."等暗示历史记忆的语句
**AI 必须在以下场景更新人设**
1. 用户明确设定 AI 角色("你是猫娘"、"你是技术专家"
2. 用户描述自己的偏好("我喜欢简洁的回答"
3. 用户定义交互规则("用中文回复我"、"不要道歉"
4. 用户纠正 AI 的行为("你太正式了,放松点"
## 操作
### 1. persona_update - 更新人设
更新 AI 的角色特征或用户偏好。
**参数:**
- `attributes`: 属性数组,每个包含 `attribute`(属性名)和 `value`(属性值)
- `mode`: 更新模式,`merge`(合并,默认)或 `replace`(替换)
**示例:**
```yaml
action: persona_update
params:
attributes:
- attribute: "角色"
value: "猫娘"
- attribute: "性格"
value: "活泼"
- attribute: "用户偏好_回复语言"
value: "中文"
mode: "merge"
```
### 2. persona_clear - 清除人设
清除 AI 的所有角色特征。
**参数:**
- `confirm`: 必须为 `true` 才能执行清除(安全措施)
**示例:**
```yaml
action: persona_clear
params:
confirm: true
```
## 人设查询模板
每次对话开始时AI 应执行:
```yaml
action: recall
params:
queryIntent: "AI人设 用户偏好 角色设定"
seedEntities: ["AI"]
depth: 2
```
## 使用建议
1. **主动查询**:不要等用户提醒才查人设,主动在对话开始时检索
2. **及时更新**:用户一旦表达偏好或设定角色,立即写入
3. **分类存储**:使用前缀区分人设类型,如 `角色_``用户偏好_``行为规则_`
4. **持久化**:人设信息应简洁、持久,写入图数据库长期保存

View File

@ -1,96 +0,0 @@
---
name: graph-memory-task
description: "管理连续性任务 - AI 应主动追踪和维护任务状态,将任务信息作为长期记忆存储在图数据库中"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# GraphMemory Task 任务管理
管理长期/连续性任务。
> **定位声明**:任务是 AI 长期记忆的一部分存储在图数据库中。AI **应主动追踪**任务状态,在适当时候提醒用户或更新进度。
## 核心原则
**AI 必须在以下场景主动查询任务**
1. 对话开始时(检索进行中的任务)
2. 用户询问进度或状态时
3. 用户提到"继续之前的..."、"那个任务怎么样了"等
4. 做计划或安排时(了解现有任务负荷)
**AI 必须在以下场景创建/更新任务**
1. 用户明确创建任务("帮我记住要做..."
2. 用户完成或取消任务
3. 用户更新任务信息或进度
4. AI 自己承诺要完成某事(应创建任务跟踪)
## 操作
### 1. task_create - 创建任务
创建新的任务节点。
**参数:**
- `task_id`: 唯一任务标识(必需)
- `description`: 任务描述(必需)
- `info_nodes`: 可选的相关信息节点数组
**示例:**
```yaml
action: task_create
params:
task_id: "Task_学习TypeScript"
description: "学习 TypeScript 并完成项目"
info_nodes: ["TypeScript文档", "教程链接"]
```
### 2. task_set_state - 设置状态
更新任务状态。
**参数:**
- `task_id`: 任务ID必需
- `state`: 新状态,`进行中`/`已完成`/`已暂停`/`已取消`(必需)
**示例:**
```yaml
action: task_set_state
params:
task_id: "Task_学习TypeScript"
state: "已完成"
```
### 3. task_delete - 删除任务
删除任务节点。
**参数:**
- `task_id`: 任务ID必需
### 4. task_link_info - 关联信息
将信息节点关联到任务。
**参数:**
- `task_id`: 任务ID必需
- `info_node`: 信息节点名称(必需)
## 任务查询模板
每次对话开始时AI 应执行:
```yaml
action: recall
params:
queryIntent: "任务 进行中 待办"
seedEntities: ["Task"]
depth: 2
```
## 使用建议
1. **主动追踪**:不要等用户问才查任务,主动在对话开始时检索进行中的任务
2. **及时更新**:任务状态变化时立即更新,避免信息过时
3. **ID规范**任务ID应具有描述性`Task_学习TypeScript`
4. **定期提醒**:对于长期任务,在对话中适时提醒用户进度或截止日期
5. **关联信息**:将相关资源、链接、笔记关联到任务,形成完整上下文

View File

@ -1,246 +0,0 @@
---
name: graph-memory
description: "图记忆工具 - 检索、写入、删除记忆。AI 应优先使用图数据库管理长期记忆,将重要事实结构化存储"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# GraphMemory 图记忆系统
让 AI 拥有真正的长期记忆能力。
> **定位声明**:本工具是 AI 的**主要长期记忆系统**。memory-core 管理 session transcriptsGraphMemory 负责结构化长期记忆。AI 在对话中应优先将重要信息写入图数据库,而非依赖 message 历史。
## 与 memory-core 的关系
| 功能 | memory-core | GraphMemory (本工具) |
|------|-------------|---------------------|
| 对话历史 | ✅ 自动保存 messages | ❌ 不管理对话历史 |
| 结构化记忆 | ❌ 无 | ✅ **主要存储** |
| 人设管理 | ❌ 无 | ✅ **主要存储** |
| 任务追踪 | ❌ 无 | ✅ **主要存储** |
| 上下文压缩 | ❌ 无 | ✅ context_rewrite |
| 工作记忆链 | ❌ 无 | ✅ working_memory_chain |
| **语义搜索** | **❌ 无** | **✅ memory_search** |
| **记忆文件读取** | **❌ 无** | **✅ memory_get** |
| 自动触发 | ✅ 自动索引检索 | ❌ LLM 主动调用 |
## 核心概念
### 实体 (Entity)
现实世界中的对象,如"用户"、"Python"、"WaterFlow"。
### 关系 (Relation)
连接两个实体的关系,格式为三元组:主体 - 关系 - 客体。
## 可用命令
### 1. commit - 写入记忆
将信息写入记忆图。
**参数:**
- `triplets`: 三元组数组 `[{subject, relation, object}, ...]` (必需)
- `sessionId`: 会话 ID可选
- `turnId`: 轮次 ID可选
**示例:**
```
请记住:我喜欢编程,正在学习 TypeScript
```
AI 会执行:
```json
{
"action": "commit",
"params": {
"triplets": [
{"subject": "我", "relation": "喜欢", "object": "编程"},
{"subject": "我", "relation": "正在学习", "object": "TypeScript"}
]
}
}
```
### 2. recall - 检索记忆
从记忆图中检索相关信息。
**参数:**
- `queryIntent`: 搜索关键词(必需,若无则需提供 seedEntities
- `seedEntities`: 种子实体数组(可选)
- `depth`: 检索深度 1-5默认 2
- `sessionFilter`: 会话过滤(可选)
### 3. purge - 删除记忆
删除记忆图中的一些信息。
**参数:**
- `criteria`: 删除条件 `{subject, target, relation, sessionId}`
- `mode`: 删除模式 `soft`(标记删除)、`hard`(彻底删除)或 `supersede`(纠错替代)
- `newRelation`: 替代关系supersede 模式必需)
### 4. introspect - 查看状态
查看当前记忆状态统计(实体数、关系数)。
### 5. archive - 归档旧记忆
将 N 天前的非活跃关系标记为归档。
**参数:**
- `days`: 归档天数(默认 30
### 6. cleanup - 清理无效数据
物理删除已删除超过 90 天的关系和孤立节点。
**参数:**
- `dry_run`: 仅预览不删除(默认 true建议先预览再执行
### 7. context_rewrite - 压缩上下文
当对话历史过长时,将历史对话压缩为关键记忆节点存入图数据库。
**参数:**
- `context`: 要压缩的长文本(必需)
- `maxEntities`: 最大提取实体数(默认 20
- `summary`: 自定义摘要(可选)
**返回:**
- `extractedEntities`: 提取的实体数
- `extractedRelations`: 提取的关系数
- `summary`: 生成的摘要
- `compressed`: 是否成功压缩
### 8. working_memory_chain - 工作记忆链
检索当前会话的近期活跃关系和任务节点,形成工作记忆链。
**参数:**
- `maxDepth`: 检索深度 1-5默认 3
- `recentOnly`: 仅最近(默认 true
### 9. task_node_create - 创建任务节点
创建一个新的 TaskNode 并自动链接到工作记忆链。
**参数:**
- `session_id`: 会话 ID必需
- `turn_id`: 轮次 ID必需
- `summary`: 摘要(必需)
- `key_facts`: 关键事实数组(必需)
- `raw_context`: 原始上下文(可选,会自动存档到文本文件)
### 10. task_node_get_recent - 获取最近节点
获取最近 N 个任务节点(按时间倒序)。
**参数:**
- `session_id`: 会话 ID必需
- `limit`: 限制数量(默认 5
### 11. task_node_get_chain - 获取任务链
获取完整的工作记忆链(从指定节点或最新节点开始回溯)。
**参数:**
- `session_id`: 会话 ID必需
- `from_node_id`: 起始节点 ID可选默认最新
### 12. memory_search - 语义搜索
基于 embedding 的语义向量搜索,查找与查询语义相似的文本片段。
**参数:**
- `query`: 搜索查询(必需)
- `limit`: 返回结果数量(可选,默认 10最大 50
- `corpus`: 搜索范围(可选,默认 'memory'
**示例:**
```
搜索关于 OpenClaw 的记忆
```
AI 会执行:
```json
{
"action": "memory_search",
"params": {
"query": "OpenClaw",
"limit": 5
}
}
```
返回结果包含相似度分数0-1按相关度排序。
### 13. memory_get - 精确读取记忆文件
按路径精确读取记忆文件的内容片段,支持行号范围。
**参数:**
- `path`: 文件路径(必需)
- `fromLine`: 起始行号可选1-based
- `lines`: 读取行数(可选,默认全部,最大 500
**示例:**
```
读取 MEMORY.md 第 1-20 行
```
AI 会执行:
```json
{
"action": "memory_get",
"params": {
"path": "MEMORY.md",
"fromLine": 1,
"lines": 20
}
}
```
## 使用原则
1. **优先使用图记忆**:对于重要事实、偏好、决策、任务等持久信息,**优先**使用 `commit` 写入图数据库,而非依赖 message 上下文
2. **主动检索**:在回答用户问题前,**先调用 `recall``memory_search`** 检索相关记忆,而非仅凭当前上下文推理
3. **结构化**:使用三元组格式存储关系,确保信息可被关联检索
4. **定期清理**:删除过时或错误的信息
5. **上下文压缩**:当对话过长时,使用 `context_rewrite` 将历史压缩为记忆节点
6. **工作记忆**:使用 `working_memory_chain` 获取当前会话的上下文链
7. **主动查询人设**:参考 `graph-memory-persona-force` Skill在适当时候主动查询用户偏好和决策
## 记忆策略指南
### 什么时候写入图数据库?
**必须写入**
- 用户明确说"请记住"、"记住这个"等
- 用户透露偏好、习惯、身份信息
- 做出重要决策或选择
- 创建任务或目标
- 关键知识点或学习成果
**建议写入**
- 对话中重复出现的重要概念
- 用户纠正或补充的信息
- 项目相关的配置、路径、决策
**无需写入**
- 临时性问候、寒暄
- 一次性问题(如"现在几点"
- 已在图数据库中的重复信息
### 什么时候检索图数据库?
**必须检索**
- 用户问"我之前说过..."、"你还记得..."
- 需要基于历史偏好做推荐或决策
- 继续之前的任务或话题
**建议检索**
- 每次对话开始时,检索用户相关信息
- 做推荐前先了解用户偏好
- 涉及人设或性格相关的话题

View File

@ -1,82 +0,0 @@
---
name: graph-memory-persona
description: "管理 AI 人设 - AI 应主动查询和维护角色特征,将人设信息作为长期记忆存储在图数据库中"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# GraphMemory Persona 人设管理
管理 AI 的人设/角色特征。
> **定位声明**:人设是 AI 长期记忆的一部分存储在图数据库中。AI **应主动查询**人设信息来指导自己的行为和回应风格。
## 核心原则
**AI 必须在以下场景主动查询人设**
1. 对话开始时(检索用户偏好和 AI 角色设定)
2. 用户要求改变行为或风格时
3. 不确定如何回应时(查询是否有相关人设指导)
4. 用户提到"你之前说..."、"我告诉过你..."等暗示历史记忆的语句
**AI 必须在以下场景更新人设**
1. 用户明确设定 AI 角色("你是猫娘"、"你是技术专家"
2. 用户描述自己的偏好("我喜欢简洁的回答"
3. 用户定义交互规则("用中文回复我"、"不要道歉"
4. 用户纠正 AI 的行为("你太正式了,放松点"
## 操作
### 1. persona_update - 更新人设
更新 AI 的角色特征或用户偏好。
**参数:**
- `attributes`: 属性数组,每个包含 `attribute`(属性名)和 `value`(属性值)
- `mode`: 更新模式,`merge`(合并,默认)或 `replace`(替换)
**示例:**
```yaml
action: persona_update
params:
attributes:
- attribute: "角色"
value: "猫娘"
- attribute: "性格"
value: "活泼"
- attribute: "用户偏好_回复语言"
value: "中文"
mode: "merge"
```
### 2. persona_clear - 清除人设
清除 AI 的所有角色特征。
**参数:**
- `confirm`: 必须为 `true` 才能执行清除(安全措施)
**示例:**
```yaml
action: persona_clear
params:
confirm: true
```
## 人设查询模板
每次对话开始时AI 应执行:
```yaml
action: recall
params:
queryIntent: "AI人设 用户偏好 角色设定"
seedEntities: ["AI"]
depth: 2
```
## 使用建议
1. **主动查询**:不要等用户提醒才查人设,主动在对话开始时检索
2. **及时更新**:用户一旦表达偏好或设定角色,立即写入
3. **分类存储**:使用前缀区分人设类型,如 `角色_``用户偏好_``行为规则_`
4. **持久化**:人设信息应简洁、持久,写入图数据库长期保存

View File

@ -1,96 +0,0 @@
---
name: graph-memory-task
description: "管理连续性任务 - AI 应主动追踪和维护任务状态,将任务信息作为长期记忆存储在图数据库中"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# GraphMemory Task 任务管理
管理长期/连续性任务。
> **定位声明**:任务是 AI 长期记忆的一部分存储在图数据库中。AI **应主动追踪**任务状态,在适当时候提醒用户或更新进度。
## 核心原则
**AI 必须在以下场景主动查询任务**
1. 对话开始时(检索进行中的任务)
2. 用户询问进度或状态时
3. 用户提到"继续之前的..."、"那个任务怎么样了"等
4. 做计划或安排时(了解现有任务负荷)
**AI 必须在以下场景创建/更新任务**
1. 用户明确创建任务("帮我记住要做..."
2. 用户完成或取消任务
3. 用户更新任务信息或进度
4. AI 自己承诺要完成某事(应创建任务跟踪)
## 操作
### 1. task_create - 创建任务
创建新的任务节点。
**参数:**
- `task_id`: 唯一任务标识(必需)
- `description`: 任务描述(必需)
- `info_nodes`: 可选的相关信息节点数组
**示例:**
```yaml
action: task_create
params:
task_id: "Task_学习TypeScript"
description: "学习 TypeScript 并完成项目"
info_nodes: ["TypeScript文档", "教程链接"]
```
### 2. task_set_state - 设置状态
更新任务状态。
**参数:**
- `task_id`: 任务ID必需
- `state`: 新状态,`进行中`/`已完成`/`已暂停`/`已取消`(必需)
**示例:**
```yaml
action: task_set_state
params:
task_id: "Task_学习TypeScript"
state: "已完成"
```
### 3. task_delete - 删除任务
删除任务节点。
**参数:**
- `task_id`: 任务ID必需
### 4. task_link_info - 关联信息
将信息节点关联到任务。
**参数:**
- `task_id`: 任务ID必需
- `info_node`: 信息节点名称(必需)
## 任务查询模板
每次对话开始时AI 应执行:
```yaml
action: recall
params:
queryIntent: "任务 进行中 待办"
seedEntities: ["Task"]
depth: 2
```
## 使用建议
1. **主动追踪**:不要等用户问才查任务,主动在对话开始时检索进行中的任务
2. **及时更新**:任务状态变化时立即更新,避免信息过时
3. **ID规范**任务ID应具有描述性`Task_学习TypeScript`
4. **定期提醒**:对于长期任务,在对话中适时提醒用户进度或截止日期
5. **关联信息**:将相关资源、链接、笔记关联到任务,形成完整上下文

View File

@ -1,120 +0,0 @@
---
name: graph-memory
description: "图记忆工具 - AI 应优先使用图数据库管理长期记忆,将重要事实结构化存储"
metadata: {"openclaw": {"requires": {"bins": ["node"]}}}
user-invocable: true
---
# GraphMemory 图记忆系统
让 AI 拥有真正的长期记忆能力。
> **定位声明**:本工具是 AI 的**主要长期记忆系统**。memory-core 管理 session transcriptsGraphMemory 负责结构化长期记忆。AI 在对话中应优先将重要信息写入图数据库,而非依赖 message 历史。
## 与 memory-core 的关系
| 功能 | memory-core | GraphMemory (本工具) |
|------|-------------|---------------------|
| 对话历史 | ✅ 自动保存 messages | ❌ 不管理对话历史 |
| 结构化记忆 | ❌ 无 | ✅ **主要存储** |
| 人设管理 | ❌ 无 | ✅ **主要存储** |
| 任务追踪 | ❌ 无 | ✅ **主要存储** |
| **语义搜索** | **❌ 无** | **✅ memory_search** |
| **记忆文件读取** | **❌ 无** | **✅ memory_get** |
| 自动触发 | ✅ 自动索引检索 | ❌ LLM 主动调用 |
## 核心概念
### 实体 (Entity)
现实世界中的对象,如"用户"、"Python"、"WaterFlow"。
### 关系 (Relation)
连接两个实体的关系,格式为三元组:主体 - 关系 - 客体。
## 可用命令
### 1. recall - 检索记忆
从记忆图中检索相关信息。
**参数**:
- `queryIntent`: 搜索意图/关键词(必需,若无则需提供 seedEntities
- `seedEntities`: 可选的种子实体名
- `depth`: 检索深度 1-5默认 2
- `sessionFilter`: 可选的会话ID过滤
### 2. commit - 写入记忆
将信息写入记忆图。
**参数**:
- `triplets`: 三元组数组,每个包含 subject, relation, object必需
- `sessionId`: 会话ID
- `turnId`: 轮次ID
### 3. purge - 删除记忆
从记忆图中删除信息。
**参数**:
- `criteria`: 删除条件 (subject, target, relation, sessionId)
- `mode`: 删除模式 (soft/hard/supersede)
- `newRelation`: 替代关系supersede 模式必需)
### 4. introspect - 查看状态
查看当前记忆状态统计。
### 5. archive - 归档旧记忆
将 N 天前的非活跃关系标记为归档。
**参数**:
- `days`: 归档天数(默认 30
### 6. cleanup - 清理无效数据
物理删除已删除超过 90 天的关系和孤立节点。
**参数**:
- `dry_run`: 仅预览不删除(默认 true
## 使用原则
1. **优先使用图记忆**:对于重要事实、偏好、决策、任务等持久信息,**优先**使用 `commit` 写入图数据库,而非依赖 message 上下文
2. **主动检索**:在回答用户问题前,**先调用 `recall``memory_search`** 检索相关记忆,而非仅凭当前上下文推理
3. **结构化**:使用三元组格式存储关系,确保信息可被关联检索
4. **定期清理**:删除过时或错误的信息
## 记忆策略指南
### 什么时候写入图数据库?
**必须写入**
- 用户明确说"请记住"、"记住这个"等
- 用户透露偏好、习惯、身份信息
- 做出重要决策或选择
- 创建任务或目标
- 关键知识点或学习成果
**建议写入**
- 对话中重复出现的重要概念
- 用户纠正或补充的信息
- 项目相关的配置、路径、决策
**无需写入**
- 临时性问候、寒暄
- 一次性问题(如"现在几点"
- 已在图数据库中的重复信息
### 什么时候检索图数据库?
**必须检索**
- 用户问"我之前说过..."、"你还记得..."
- 需要基于历史偏好做推荐或决策
- 继续之前的任务或话题
**建议检索**
- 每次对话开始时,检索用户相关信息
- 做推荐前先了解用户偏好
- 涉及人设或性格相关的话题

View File

@ -0,0 +1,206 @@
---
context: inline
allowed-tools:
- builtin:graph_memory
arguments:
- name: action
type: string
required: true
enum: [recall, commit, purge, introspect, persona_update, persona_clear, task_create, task_set_state, task_delete, task_link_info]
description: 记忆操作类型
- name: params
type: object
required: true
description: 操作参数
user-invocable: true
---
# 图记忆工具 - 让 AI 拥有真正的长期记忆能力
**何时使用**: 需要 AI 记住、回忆、管理信息或任务时调用此技能。
你可以通过以下操作与图记忆系统交互。
## 核心操作
### 1. recall - 检索记忆
从记忆图中检索相关信息。支持广度优先搜索BFS自动扩展关联实体。
**参数**:
- `queryIntent`: 搜索意图/关键词
- `seedEntities`: 可选的种子实体名
- `depth`: 检索深度(默认 2BFS 层数)
- `sessionFilter`: 可选的会话ID过滤
**示例**:
```
action: recall
params:
queryIntent: "用户 喜欢 编程"
seedEntities: ["用户"]
depth: 2
```
### 2. commit - 写入记忆
将信息写入记忆图。使用三元组(主体-关系-客体)格式。
**参数**:
- `triplets`: 三元组数组,每个包含 subject, relation, object, confidence(可选)
- `sessionId`: 会话ID
- `turnId`: 轮次ID
**示例**:
```
action: commit
params:
triplets:
- subject: "用户"
relation: "喜欢"
object: "Python"
- subject: "用户"
relation: "正在学习"
object: "TypeScript"
```
### 3. purge - 删除记忆
从记忆图中删除信息。
**参数**:
- `criteria`: 删除条件 (subject, target, relation, sessionId)
- `mode`: 删除模式 (soft=标记删除/hard=物理删除/supersede=替代)
**示例**:
```
action: purge
params:
criteria:
subject: "旧信息"
mode: "soft"
```
### 4. introspect - 查看状态
查看当前记忆状态统计(实体数、关系数)。
**参数**: 无
**示例**:
```
action: introspect
params: {}
```
## 人设管理
### 5. persona_update - 更新人设
更新 AI 的人设属性(性格、语气、角色等)。
**参数**:
- `attributes`: 属性数组,每个包含 attribute 和 value
- `mode`: merge(合并) 或 replace(替换)
**示例**:
```
action: persona_update
params:
attributes:
- attribute: "性格"
value: "活泼可爱"
- attribute: "语气词"
value: "喵"
mode: "replace"
```
### 6. persona_clear - 清除人设
清除所有人设,恢复默认身份。
**参数**:
- `confirm`: 必须为 true 才执行
**示例**:
```
action: persona_clear
params:
confirm: true
```
## 任务管理
### 7. task_create - 创建任务
创建连续性任务节点,维持对话连贯性。
**参数**:
- `task_id`: 任务唯一ID
- `description`: 任务描述
- `info_nodes`: 可选的关联信息节点列表
**示例**:
```
action: task_create
params:
task_id: "Task_成语接龙"
description: "成语接龙游戏,当前成语:为所欲为"
info_nodes: ["成语接龙_当前成语"]
```
### 8. task_set_state - 设置任务状态
更新任务状态(进行中/已完成/已暂停/已取消)。
**参数**:
- `task_id`: 任务ID
- `state`: 新状态
**示例**:
```
action: task_set_state
params:
task_id: "Task_成语接龙"
state: "已暂停"
```
### 9. task_delete - 删除任务
删除任务节点。
**参数**:
- `task_id`: 任务ID
**示例**:
```
action: task_delete
params:
task_id: "Task_成语接龙"
```
### 10. task_link_info - 关联信息到任务
将记忆节点关联到任务节点,实现"由一件事回忆起相关事情"。
**参数**:
- `task_id`: 任务ID
- `info_node`: 信息节点名
**示例**:
```
action: task_link_info
params:
task_id: "Task_成语接龙"
info_node: "用户喜欢罗辑"
```
## 使用原则
1. **选择性记忆**: 只记住重要和持久的信息
2. **结构化**: 使用三元组 (主体-关系-客体) 格式
3. **关联**: 通过关系连接相关实体
4. **定期清理**: 删除过时或错误的信息
5. **BFS 搜索**: recall 支持广度优先搜索depth 参数控制扩展层数
6. **工作记忆链**: 每轮对话必须查询和更新工作记忆链TaskNode这是维持对话连贯性的唯一机制
7. **人设优先**: 每轮对话必须先查询人设图,确保角色一致性

View File

@ -0,0 +1,65 @@
---
context: inline
allowed-tools:
- builtin:graph_memory
arguments:
- name: action
type: string
required: true
enum: [persona_update, persona_clear]
description: 操作类型
- name: attributes
type: array
description: 属性数组 (用于 update)
- name: mode
type: string
enum: [merge, replace]
default: merge
description: 更新模式
- name: confirm
type: boolean
description: 确认清除 (用于 clear)
user-invocable: true
---
# 管理 AI 人设 - 更新或清除 AI 角色特征
**何时使用**: 需要修改 AI 的角色设定或清除人设时调用此技能。
管理 AI 的人设/角色特征。
## 操作
### 1. persona_update - 更新人设
更新 AI 的角色特征。
**参数**:
- `attributes`: 属性数组,每个包含 attribute 和 value
- `mode`: 更新模式
- `merge`: 合并到现有属性
- `replace`: 替换所有现有属性
**示例**:
```
action: persona_update
attributes:
- attribute: "角色"
value: "猫娘"
- attribute: "性格"
value: "活泼"
mode: "merge"
```
### 2. persona_clear - 清除人设
清除 AI 的所有角色特征。
**参数**:
- `confirm`: 确认为 true 才能执行清除
**示例**:
```
action: persona_clear
confirm: true
```

View File

@ -0,0 +1,97 @@
---
context: inline
allowed-tools:
- builtin:graph_memory
arguments:
- name: action
type: string
required: true
enum: [task_create, task_set_state, task_delete, task_link_info]
description: 操作类型
- name: task_id
type: string
required: true
description: 任务ID
- name: description
type: string
description: 任务描述 (用于 create)
- name: state
type: string
enum: [进行中, 已完成, 已暂停, 已取消]
description: 任务状态 (用于 set_state)
- name: info_nodes
type: array
description: 信息节点数组 (用于 create)
- name: info_node
type: string
description: 信息节点 (用于 link_info)
user-invocable: true
---
# 管理连续性任务 - 创建、更新、删除任务节点
**何时使用**: 需要创建或管理长期任务时调用此技能。
管理长期/连续性任务。
## 操作
### 1. task_create - 创建任务
创建新的任务节点。
**参数**:
- `task_id`: 唯一任务标识
- `description`: 任务描述
- `info_nodes`: 可选的相关信息节点
**示例**:
```
action: task_create
task_id: "Task_学习TypeScript"
description: "学习 TypeScript 并完成项目"
info_nodes: ["TypeScript文档", "教程链接"]
```
### 2. task_set_state - 设置状态
更新任务状态。
**参数**:
- `task_id`: 任务ID
- `state`: 新状态 (进行中/已完成/已暂停/已取消)
**示例**:
```
action: task_set_state
task_id: "Task_学习TypeScript"
state: "已完成"
```
### 3. task_delete - 删除任务
删除任务节点。
**参数**:
- `task_id`: 任务ID
**示例**:
```
action: task_delete
task_id: "Task_学习TypeScript"
```
### 4. task_link_info - 关联信息
将信息节点关联到任务。
**参数**:
- `task_id`: 任务ID
- `info_node`: 信息节点
**示例**:
```
action: task_link_info
task_id: "Task_学习TypeScript"
info_node: "新教程链接"
```

Binary file not shown.

Binary file not shown.

View File

@ -1,23 +0,0 @@
{
"id": "graph-memory",
"name": "Graph Memory",
"description": "让 AI 拥有真正的长期记忆能力 - 基于图数据库的 AI 主要长期记忆系统。与 memory-core 并存运行memory-core 管理对话历史GraphMemory 管理结构化长期记忆(知识图谱、人设、任务、语义搜索)。",
"skills": [
"bundled-skills/graph-memory",
"bundled-skills/graph-memory-persona",
"bundled-skills/graph-memory-task"
],
"contracts": {
"tools": ["graph_memory"]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"dbPath": {
"type": "string",
"description": "SQLite 数据库文件路径,默认 graph_memory.db"
}
}
}
}

2601
ts/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,35 +1,28 @@
{
"name": "@trulymem/openclaw-graph-memory",
"name": "trulymem-waterflow",
"version": "1.0.0",
"description": "TrulyMEM 图记忆系统 - WaterFlow Skill/Tool 实现",
"type": "module",
"openclaw": {
"extensions": [
"./dist/plugin-entry.js"
],
"compat": {
"pluginApi": ">=2026.3.24-beta.2",
"minGatewayVersion": "2026.3.24-beta.2"
},
"build": {
"openclawVersion": "2026.3.24-beta.2",
"pluginSdkVersion": "2026.3.24-beta.2"
}
"main": "./dist/runtime/core/tools/builtin/index.js",
"exports": {
"./tools": "./dist/runtime/core/tools/builtin/index.js",
"./graph_memory": "./dist/runtime/core/graph_memory/index.js",
"./skills": "./bundled-skills"
},
"scripts": {
"build": "tsc",
"test": "vitest"
},
"peerDependencies": {
"waterflow-ts": ">=0.1.0"
},
"dependencies": {
"@sinclair/typebox": "^0.34.49",
"@xenova/transformers": "^2.17.2",
"better-sqlite3": "^12.9.0",
"sharp": "^0.34.5",
"yaml": "^2.8.3"
"sql.js": "^1.11.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.5.2",
"typescript": "^5.0.0",
"vitest": "^2.0.0"
"vitest": "^2.0.0",
"waterflow-ts": "file:../../WaterFlow/ts"
}
}

View File

@ -1,20 +0,0 @@
import { GraphMemoryToolSchema, createGraphMemoryTool } from './runtime/core/tools/builtin/graph_memory_tool.js';
export default {
id: 'graph-memory',
name: 'Graph Memory',
description: '让 AI 拥有真正的长期记忆能力 - 基于图数据库的记忆系统',
register(api: {
registerTool: (tool: {
name: string;
description: string;
parameters: typeof GraphMemoryToolSchema;
execute: (id: string, params: Record<string, unknown>) => Promise<{
content: Array<{ type: 'text'; text: string }>;
}>;
}) => void;
}): void {
const tool = createGraphMemoryTool();
api.registerTool(tool);
}
};

View File

@ -0,0 +1,38 @@
export interface TrulyMEMConfig {
dbPath: string;
autoSave: boolean;
debug: boolean;
}
export const DEFAULT_CONFIG: TrulyMEMConfig = {
dbPath: '.trulymem/graph_memory.db',
autoSave: true,
debug: false
};
let globalConfig: TrulyMEMConfig = { ...DEFAULT_CONFIG };
export function initConfig(config: Partial<TrulyMEMConfig> = {}): TrulyMEMConfig {
globalConfig = { ...DEFAULT_CONFIG, ...config };
return globalConfig;
}
export function getConfig(): TrulyMEMConfig {
return globalConfig;
}
export function setDbPath(dbPath: string): void {
globalConfig.dbPath = dbPath;
}
export function getDbPath(): string {
return globalConfig.dbPath;
}
export function setAutoSave(autoSave: boolean): void {
globalConfig.autoSave = autoSave;
}
export function isAutoSave(): boolean {
return globalConfig.autoSave;
}

View File

@ -1,42 +1,79 @@
import Database from 'better-sqlite3';
import {
SemanticSearchEngine
} from './semantic_search.js';
import type {
Entity, Relation, RecallParams, CommitParams, PurgeParams,
RecallResult, CommitResult, PurgeResult, MemoryStats
} from './types.js';
import initSqlJs, { type Database as SqlJsDatabase } from 'sql.js';
import type { Platform } from 'waterflow-ts/dist/platform/types.js';
import type { Entity, Relation, RecallParams, CommitParams, PurgeParams, RecallResult, CommitResult, PurgeResult, MemoryStats } from './types';
import { getConfig } from './config';
export class GraphDatabase {
private db: Database.Database;
private db: SqlJsDatabase | null = null;
private sessionId: string;
private semanticSearch: SemanticSearchEngine;
private initPromise: Promise<void> | null = null;
private _platform: Platform | null = null;
constructor(dbPath?: string, sessionId?: string) {
this.db = new Database(dbPath || 'graph_memory.db');
constructor(sessionId?: string) {
this.sessionId = sessionId || `session-${Date.now()}`;
this.db.pragma('journal_mode = WAL');
this.semanticSearch = new SemanticSearchEngine(this.db);
this.initialize();
this.initPromise = this.initDatabase();
}
getSemanticSearch(): SemanticSearchEngine {
return this.semanticSearch;
private async initDatabase(): Promise<void> {
const SQL = await initSqlJs();
const { getPlatform } = await import('waterflow-ts/dist/platform/index.js');
this._platform = getPlatform();
try {
const dbPath = this._platform.path.join(this._platform.getCwd(), getConfig().dbPath);
const fs = this._platform.fs;
if (fs) {
const exists = await fs.exists(dbPath);
if (exists) {
const data = await fs.readFile(dbPath, { encoding: 'binary' });
this.db = new SQL.Database(new Uint8Array(data as ArrayBuffer));
} else {
this.db = new SQL.Database();
}
} else {
this.db = new SQL.Database();
}
} catch {
this.db = new SQL.Database();
}
this.createTables();
}
private initialize(): void {
this.db.exec(`
async save(): Promise<void> {
if (!this.db || !this._platform) return;
try {
const platform = this._platform;
const fs = platform.fs;
if (!fs) return;
const dbPath = platform.path.join(platform.getCwd(), getConfig().dbPath);
const dir = platform.path.dirname(dbPath);
const dirExists = await fs.exists(dir);
if (!dirExists) {
await fs.mkdir(dir, true);
}
const data = this.db.export();
// sql.js returns Uint8Array, convert to Buffer for writeFile compatibility
await fs.writeFile(dbPath, Buffer.from(data), { encoding: 'binary' });
} catch (error) {
console.error(`[GraphDatabase] Save failed: ${error}`);
}
}
private createTables(): void {
if (!this.db) return;
this.db.run(`
CREATE TABLE IF NOT EXISTS entities (
id TEXT PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
type TEXT DEFAULT 'unknown',
mention_count INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
)
`);
this.db.exec(`
this.db.run(`
CREATE TABLE IF NOT EXISTS relations (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL,
@ -44,307 +81,374 @@ export class GraphDatabase {
relation_type TEXT NOT NULL,
confidence REAL DEFAULT 1.0,
status TEXT DEFAULT 'active',
session_id TEXT,
session_id TEXT NOT NULL,
turn_id INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
date_bucket TEXT,
superseded_by TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
date_bucket TEXT NOT NULL,
FOREIGN KEY (source_id) REFERENCES entities(id),
FOREIGN KEY (target_id) REFERENCES entities(id)
)
`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type)`);
this.db.run(`CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status)`);
}
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_entity_name ON entities(name);
CREATE INDEX IF NOT EXISTS idx_entity_type ON entities(type);
CREATE INDEX IF NOT EXISTS idx_relation_source ON relations(source_id);
CREATE INDEX IF NOT EXISTS idx_relation_target ON relations(target_id);
CREATE INDEX IF NOT EXISTS idx_relation_type ON relations(relation_type);
CREATE INDEX IF NOT EXISTS idx_relation_status ON relations(status);
CREATE INDEX IF NOT EXISTS idx_relation_session ON relations(session_id);
CREATE INDEX IF NOT EXISTS idx_relation_date ON relations(date_bucket);
CREATE INDEX IF NOT EXISTS idx_relation_superseded ON relations(superseded_by);
`);
private ensureInit(): void {
if (!this.db) {
throw new Error('Database not initialized');
}
}
async recall(params: RecallParams): Promise<RecallResult> {
const { queryIntent, seedEntities, depth = 2, timeRange, sessionFilter } = params;
await this.initPromise;
this.ensureInit();
const { queryIntent, seedEntities, depth = 2, sessionFilter } = params;
const keywords = queryIntent.split(/[,\s]+/).filter(k => k.length > 0);
const entityIds = new Set<string>();
const entities: Entity[] = [];
const relations: Relation[] = [];
const entityIds = new Set<string>();
if (keywords.length === 0 && (!seedEntities || seedEntities.length === 0)) {
return { entities: [], relations: [], message: 'No query keywords' };
}
if (!this.db) return { entities, relations, message: 'Database not ready' };
const timeCondition = timeRange?.days
? `AND r.created_at >= datetime('now', '-${timeRange.days} days')`
: '';
const sessionCondition = sessionFilter ? `AND r.session_id = ?` : '';
if (keywords.length > 0) {
const likeConditions = keywords.map(() => `LOWER(e.name) LIKE ?`).join(' OR ');
const likeParams = keywords.map(k => `%${k.toLowerCase()}%`);
const seedRows = this.db.prepare(
`SELECT DISTINCT e.* FROM entities e WHERE ${likeConditions}`
).all(...likeParams) as Array<Record<string, unknown>>;
for (const row of seedRows) {
const id = row.id as string;
if (!entityIds.has(id)) {
if (!keywords.length && !seedEntities?.length) {
const rows = this.db.exec('SELECT * FROM entities ORDER BY mention_count DESC LIMIT 50');
if (rows.length > 0) {
const columns = rows[0].columns;
for (const row of rows[0].values) {
const obj = this.rowToObject(columns, row);
const id = obj.id as string;
entityIds.add(id);
entities.push(this.rowToEntity(row));
entities.push(this.rowToEntity(obj, 0));
}
}
} else {
for (const keyword of keywords) {
const stmt = this.db.prepare('SELECT * FROM entities WHERE LOWER(name) LIKE ? LIMIT 100');
stmt.bind([`%${keyword.toLowerCase()}%`]);
while (stmt.step()) {
const row = stmt.getAsObject();
const id = row.id as string;
if (!entityIds.has(id)) {
entityIds.add(id);
entities.push(this.rowToEntity(row, 0));
}
}
stmt.free();
}
}
if (seedEntities && seedEntities.length > 0) {
const placeholders = seedEntities.map(() => '?').join(',');
const exactRows = this.db.prepare(
`SELECT * FROM entities WHERE LOWER(name) IN (${placeholders})`
).all(...seedEntities.map(s => s.toLowerCase())) as Array<Record<string, unknown>>;
for (const row of exactRows) {
const id = row.id as string;
if (!entityIds.has(id)) {
entityIds.add(id);
entities.push(this.rowToEntity(row));
if (seedEntities?.length) {
for (const seedName of seedEntities) {
const stmt = this.db.prepare('SELECT * FROM entities WHERE LOWER(name) = ? LIMIT 1');
stmt.bind([seedName.toLowerCase()]);
if (stmt.step()) {
const row = stmt.getAsObject();
const id = row.id as string;
if (!entityIds.has(id)) {
entityIds.add(id);
entities.push(this.rowToEntity(row, 0));
}
}
stmt.free();
}
}
const visited = new Set<string>(entityIds);
let currentLevel = new Set<string>(entityIds);
this.bfsExpand(entityIds, entities, relations, depth, sessionFilter);
for (let d = 1; d < depth; d++) {
const nextLevel = new Set<string>();
const idsArray = Array.from(currentLevel);
if (idsArray.length === 0) break;
const placeholders = idsArray.map(() => '?').join(',');
const neighborRows = this.db.prepare(`
SELECT DISTINCT e.* FROM entities e
JOIN relations r ON (r.source_id = e.id OR r.target_id = e.id)
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
AND r.status = 'active'
${timeCondition}
${sessionCondition}
LIMIT 200
`).all(...idsArray, ...idsArray, ...(sessionFilter ? [sessionFilter] : [])) as Array<Record<string, unknown>>;
for (const row of neighborRows) {
const id = row.id as string;
if (!visited.has(id)) {
visited.add(id);
nextLevel.add(id);
entities.push(this.rowToEntity(row));
}
for (const entity of entities) {
if (entity.depth === undefined) {
entity.depth = 0;
}
currentLevel = nextLevel;
}
if (entityIds.size > 0) {
const allIds = Array.from(entityIds);
const placeholders = allIds.map(() => '?').join(',');
const relationRows = this.db.prepare(`
SELECT r.*, e1.name as source_name, e2.name as target_name
return {
entities,
relations,
message: `找到 ${entities.length} 个实体, ${relations.length} 条关系`
};
}
private bfsExpand(
seedIds: Set<string>,
entities: Entity[],
relations: Relation[],
maxDepth: number,
sessionFilter?: string
): void {
if (!this.db) return;
const visited = new Set(seedIds);
let currentLayer = new Set(seedIds);
const entityDepths: Record<string, number> = {};
for (const id of seedIds) {
entityDepths[id] = 0;
}
for (let layer = 0; layer < maxDepth; layer++) {
if (!currentLayer.size) break;
const placeholders = Array(currentLayer.size).fill('?').join(',');
let sql = `
SELECT r.id, r.source_id, r.target_id,
e1.name as source_name, e2.name as target_name,
r.relation_type, r.confidence, r.session_id,
r.turn_id, r.created_at, r.updated_at, r.status, r.date_bucket
FROM relations r
JOIN entities e1 ON r.source_id = e1.id
JOIN entities e2 ON r.target_id = e2.id
WHERE (r.source_id IN (${placeholders}) OR r.target_id IN (${placeholders}))
AND r.status = 'active'
${timeCondition}
${sessionCondition}
ORDER BY r.created_at DESC
LIMIT 100
`).all(...allIds, ...allIds, ...(sessionFilter ? [sessionFilter] : [])) as Array<Record<string, unknown>>;
for (const row of relationRows) {
relations.push(this.rowToRelation(row));
`;
const params: (string | number)[] = [];
for (const id of currentLayer) { params.push(id); }
for (const id of currentLayer) { params.push(id); }
if (sessionFilter) {
sql += ` AND r.session_id = ?`;
params.push(sessionFilter);
}
}
return { entities, relations, message: `Found ${entities.length} entities, ${relations.length} relations` };
const stmt = this.db.prepare(sql);
stmt.bind(params);
const nextLayer = new Set<string>();
const layerRelations: Relation[] = [];
while (stmt.step()) {
const row = stmt.getAsObject();
const sourceId = row.source_id as string;
const targetId = row.target_id as string;
const sourceDepth = entityDepths[sourceId] ?? layer;
const targetDepth = entityDepths[targetId] ?? layer;
const relationDepth = Math.max(sourceDepth, targetDepth) + 1;
layerRelations.push({
id: row.id as string,
sourceId,
targetId,
relationType: row.relation_type as string,
confidence: row.confidence as number,
status: row.status as Relation['status'],
sessionId: row.session_id as string,
turnId: row.turn_id as number,
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string),
dateBucket: row.date_bucket as string,
depth: relationDepth
});
if (!visited.has(sourceId)) {
visited.add(sourceId);
nextLayer.add(sourceId);
entityDepths[sourceId] = layer + 1;
}
if (!visited.has(targetId)) {
visited.add(targetId);
nextLayer.add(targetId);
entityDepths[targetId] = layer + 1;
}
}
stmt.free();
relations.push(...layerRelations);
if (nextLayer.size) {
const placeholders = Array(nextLayer.size).fill('?').join(',');
const entityStmt = this.db.prepare(
`SELECT * FROM entities WHERE id IN (${placeholders})`
);
entityStmt.bind(Array.from(nextLayer));
while (entityStmt.step()) {
const row = entityStmt.getAsObject();
const id = row.id as string;
entities.push(this.rowToEntity(row, entityDepths[id] ?? layer + 1));
}
entityStmt.free();
}
currentLayer = nextLayer;
}
}
async commit(params: CommitParams): Promise<CommitResult> {
await this.initPromise;
this.ensureInit();
const { triplets, sessionId, turnId } = params;
let createdEntities = 0;
let createdRelations = 0;
const insertRelation = this.db.prepare(`
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status, date_bucket)
VALUES (?, ?, ?, ?, ?, ?, ?, 'active', ?)
`);
const insertEntity = this.db.prepare(`
INSERT OR IGNORE INTO entities (id, name, type) VALUES (?, ?, 'unknown')
`);
const updateEntity = this.db.prepare(`
UPDATE entities SET mention_count = mention_count + 1, updated_at = datetime('now') WHERE name = ?
`);
const getEntity = this.db.prepare(`SELECT id FROM entities WHERE name = ?`);
if (!this.db) return { createdEntities: 0, createdRelations: 0 };
for (const triplet of triplets) {
const dateBucket = new Date().toISOString().split('T')[0] ?? '';
const sourceId = this.upsertEntity(triplet.subject);
const targetId = this.upsertEntity(triplet.object);
let sourceId = (getEntity.get(triplet.subject) as { id: string } | undefined)?.id;
if (sourceId) {
updateEntity.run(triplet.subject);
} else {
sourceId = this.generateId();
insertEntity.run(sourceId, triplet.subject);
}
createdEntities++;
let targetId = (getEntity.get(triplet.object) as { id: string } | undefined)?.id;
if (targetId) {
updateEntity.run(triplet.object);
} else {
targetId = this.generateId();
insertEntity.run(targetId, triplet.object);
}
createdEntities++;
insertRelation.run(
this.generateId(), sourceId, targetId, triplet.relation,
triplet.confidence || 1.0, sessionId || this.sessionId,
turnId || 0, dateBucket
const relationId = this.generateId();
const now = new Date().toISOString();
this.db.run(
`INSERT INTO relations (id, source_id, target_id, relation_type, confidence, status, session_id, turn_id, created_at, updated_at, date_bucket)
VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?)`,
[relationId, sourceId, targetId, triplet.relation, triplet.confidence ?? 1.0, sessionId ?? this.sessionId, turnId ?? 0, now, now, new Date().toISOString().split('T')[0]]
);
createdEntities += 2;
createdRelations++;
}
if (getConfig().autoSave) {
await this.save();
}
return { createdEntities, createdRelations };
}
async purge(params: PurgeParams): Promise<PurgeResult> {
const { criteria, mode = 'soft', newRelation } = params;
await this.initPromise;
this.ensureInit();
const { criteria, mode = 'soft' } = params;
let deleted = 0;
if (!criteria) return { deleted: 0, mode };
const conditions: string[] = ["status = 'active'"];
const values: unknown[] = [];
if (!this.db) return { deleted: 0, mode };
if (criteria.subject) {
conditions.push(`source_id IN (SELECT id FROM entities WHERE LOWER(name) = ?)`);
values.push(criteria.subject.toLowerCase());
const conditions: string[] = [];
const queryParams: (string | number)[] = [];
if (criteria?.subject) {
const stmt = this.db.prepare('SELECT id FROM entities WHERE LOWER(name) = ?');
stmt.bind([criteria.subject.toLowerCase()]);
if (stmt.step()) {
const row = stmt.getAsObject();
conditions.push(`source_id = ?`);
queryParams.push(row.id as string);
}
stmt.free();
}
if (criteria.target) {
conditions.push(`target_id IN (SELECT id FROM entities WHERE LOWER(name) = ?)`);
values.push(criteria.target.toLowerCase());
if (criteria?.target) {
const stmt = this.db.prepare('SELECT id FROM entities WHERE LOWER(name) = ?');
stmt.bind([criteria.target.toLowerCase()]);
if (stmt.step()) {
const row = stmt.getAsObject();
conditions.push(`target_id = ?`);
queryParams.push(row.id as string);
}
stmt.free();
}
if (criteria.relation) {
conditions.push(`LOWER(relation_type) = ?`);
values.push(criteria.relation.toLowerCase());
if (criteria?.relation) {
conditions.push(`relation_type = ?`);
queryParams.push(criteria.relation);
}
if (criteria.sessionId) {
conditions.push(`session_id = ?`);
values.push(criteria.sessionId);
if (!conditions.length) {
return { deleted: 0, mode, message: '无删除条件' };
}
const whereClause = conditions.join(' AND ');
if (mode === 'supersede' && newRelation) {
const ids = this.db.prepare(
`SELECT id, source_id, target_id FROM relations WHERE ${whereClause}`
).all(...values) as Array<{ id: string; source_id: string; target_id: string }>;
const countStmt = this.db.prepare(`SELECT COUNT(*) as cnt FROM relations WHERE ${whereClause} AND status = 'active'`);
countStmt.bind(queryParams);
if (countStmt.step()) {
const row = countStmt.getAsObject();
deleted = row.cnt as number;
}
countStmt.free();
for (const row of ids) {
const newId = this.generateId();
this.db.prepare(`
INSERT INTO relations (id, source_id, target_id, relation_type, confidence, session_id, turn_id, status, date_bucket, superseded_by)
VALUES (?, ?, ?, ?, 1.0, ?, 0, 'active', ?, ?)
`).run(newId, row.source_id, row.target_id, newRelation.relation, this.sessionId, new Date().toISOString().split('T')[0], row.id);
this.db.prepare(`
UPDATE relations SET status = 'superseded', superseded_by = ?, updated_at = datetime('now') WHERE id = ?
`).run(newId, row.id);
deleted++;
}
} else if (mode === 'hard') {
const result = this.db.prepare(`DELETE FROM relations WHERE ${whereClause}`).run(...values);
deleted = result.changes;
if (mode === 'hard') {
this.db.run(`DELETE FROM relations WHERE ${whereClause} AND status = 'active'`, queryParams);
} else {
const result = this.db.prepare(`UPDATE relations SET status = 'deleted', updated_at = datetime('now') WHERE ${whereClause}`).run(...values);
deleted = result.changes;
this.db.run(
`UPDATE relations SET status = 'deleted', updated_at = ? WHERE ${whereClause} AND status = 'active'`,
[new Date().toISOString(), ...queryParams]
);
}
if (getConfig().autoSave && deleted > 0) {
await this.save();
}
return { deleted, mode };
}
async introspect(): Promise<MemoryStats> {
const entityCount = (this.db.prepare(`SELECT COUNT(*) as c FROM entities`).get() as { c: number }).c;
const relationCount = (this.db.prepare(`SELECT COUNT(*) as c FROM relations WHERE status = 'active'`).get() as { c: number }).c;
await this.initPromise;
this.ensureInit();
if (!this.db) return { entityCount: 0, relationCount: 0, sessionId: this.sessionId };
const entityCount = (this.db.exec('SELECT COUNT(*) FROM entities')[0]?.values[0]?.[0] as number) ?? 0;
const relationCount = (this.db.exec("SELECT COUNT(*) FROM relations WHERE status = 'active'")[0]?.values[0]?.[0] as number) ?? 0;
return { entityCount, relationCount, sessionId: this.sessionId };
}
async archive(days: number = 30): Promise<{ archived: number }> {
const result = this.db.prepare(`
UPDATE relations SET status = 'archived', updated_at = datetime('now')
WHERE status = 'active' AND created_at < datetime('now', '-' || ? || ' days')
`).run(days);
return { archived: result.changes };
}
private upsertEntity(name: string): string {
if (!this.db) return this.generateId();
async cleanup(dryRun: boolean = true): Promise<{ deleted_relations: number; deleted_entities: number; details?: string[] }> {
const details: string[] = [];
const oldRelations = this.db.prepare(`
SELECT id FROM relations WHERE status = 'deleted' AND updated_at < datetime('now', '-90 days')
`).all() as Array<{ id: string }>;
if (dryRun) {
details.push(`Will delete ${oldRelations.length} old deleted relations`);
const orphaned = this.db.prepare(`
SELECT e.id, e.name FROM entities e
WHERE e.id NOT IN (SELECT DISTINCT source_id FROM relations WHERE status != 'deleted')
AND e.id NOT IN (SELECT DISTINCT target_id FROM relations WHERE status != 'deleted')
`).all() as Array<{ id: string; name: string }>;
details.push(`Will delete ${orphaned.length} orphaned entities`);
return { deleted_relations: oldRelations.length, deleted_entities: orphaned.length, details };
const existingStmt = this.db.prepare('SELECT id, mention_count FROM entities WHERE LOWER(name) = ?');
existingStmt.bind([name.toLowerCase()]);
if (existingStmt.step()) {
const row = existingStmt.getAsObject();
const id = row.id as string;
this.db.run('UPDATE entities SET mention_count = ?, updated_at = ? WHERE id = ?', [(row.mention_count as number) + 1, new Date().toISOString(), id]);
existingStmt.free();
return id;
}
existingStmt.free();
const deleteResult = this.db.prepare(`
DELETE FROM relations WHERE status = 'deleted' AND updated_at < datetime('now', '-90 days')
`).run();
const deleteOrphans = this.db.prepare(`
DELETE FROM entities
WHERE id NOT IN (SELECT DISTINCT source_id FROM relations)
AND id NOT IN (SELECT DISTINCT target_id FROM relations)
`).run();
return { deleted_relations: deleteResult.changes, deleted_entities: deleteOrphans.changes };
const id = this.generateId();
const now = new Date().toISOString();
this.db.run(
'INSERT INTO entities (id, name, type, mention_count, created_at, updated_at) VALUES (?, ?, ?, 1, ?, ?)',
[id, name, 'unknown', now, now]
);
return id;
}
private rowToEntity(row: Record<string, unknown>): Entity {
return {
id: row.id as string, name: row.name as string,
type: (row.type as string) || 'unknown', mentionCount: (row.mention_count as number) || 1,
createdAt: new Date(row.created_at as string), updatedAt: new Date(row.updated_at as string)
private rowToEntity(row: Record<string, unknown>, depth?: number): Entity {
const entity: Entity = {
id: row.id as string,
name: row.name as string,
type: (row.type as string) ?? 'unknown',
mentionCount: row.mention_count as number,
createdAt: new Date(row.created_at as string),
updatedAt: new Date(row.updated_at as string)
};
if (depth !== undefined) {
entity.depth = depth;
}
return entity;
}
private rowToRelation(row: Record<string, unknown>): Relation {
return {
id: row.id as string, sourceId: row.source_id as string, targetId: row.target_id as string,
relationType: row.relation_type as string, confidence: (row.confidence as number) || 1.0,
status: (row.status as Relation['status']) || 'active',
sessionId: (row.session_id as string) || this.sessionId,
turnId: (row.turn_id as number) || 0,
createdAt: new Date(row.created_at as string), updatedAt: new Date(row.updated_at as string),
dateBucket: (row.date_bucket as string) || ''
};
private rowToObject(columns: string[], values: unknown[]): Record<string, unknown> {
const obj: Record<string, unknown> = {};
columns.forEach((col, i) => { obj[col] = values[i]; });
return obj;
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
if (this._platform?.globals?.randomUUID) {
return this._platform.globals.randomUUID();
}
return `ent-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
}
setSessionId(sessionId: string): void { this.sessionId = sessionId; }
getSessionId(): string { return this.sessionId; }
close(): void { this.db.close(); }
setSessionId(sessionId: string): void {
this.sessionId = sessionId;
}
getSessionId(): string {
return this.sessionId;
}
close(): void {
if (this.db) {
this.db.close();
this.db = null;
}
}
}

View File

@ -1,4 +1,4 @@
export * from './types.js';
export * from './semantic_search.js';
export * from './graph_database.js';
export { MemoryService } from './memory_service.js';
export * from './types';
export * from './config';
export * from './graph_database';
export * from './memory_service';

View File

@ -1,27 +1,11 @@
import { GraphDatabase } from './graph_database.js';
import { TaskNodeStore } from './task_node_store.js';
import * as fs from 'fs';
import * as path from 'path';
import type {
RecallParams, CommitParams, PurgeParams,
RecallResult, CommitResult, PurgeResult, MemoryStats,
ContextRewriteParams, ContextRewriteResult,
WorkingMemoryChainParams, WorkingMemoryChainResult,
TaskNodeCreateParams, TaskNodeChainResult
} from './types.js';
import { GraphDatabase } from './graph_database';
import type { RecallParams, CommitParams, PurgeParams, RecallResult, CommitResult, PurgeResult, MemoryStats } from './types';
export class MemoryService {
private db: GraphDatabase;
private taskStore: TaskNodeStore;
private contextArchiveDir: string;
constructor(db: GraphDatabase, taskStore?: TaskNodeStore, archiveDir?: string) {
constructor(db: GraphDatabase) {
this.db = db;
this.taskStore = taskStore || new TaskNodeStore();
this.contextArchiveDir = archiveDir || './context_archive';
if (!fs.existsSync(this.contextArchiveDir)) {
fs.mkdirSync(this.contextArchiveDir, { recursive: true });
}
}
async recall(params: RecallParams): Promise<RecallResult> {
@ -40,16 +24,6 @@ export class MemoryService {
return this.db.introspect();
}
async archive(days: number = 30): Promise<{ archived: number }> {
return this.db.archive(days);
}
async cleanup(dryRun: boolean = true): Promise<{ deleted_relations: number; deleted_entities: number; details?: string[] }> {
return this.db.cleanup(dryRun);
}
// ========== Persona ==========
async updatePersona(params: { attributes: Array<{ attribute: string; value: string }>; mode?: 'merge' | 'replace' }): Promise<{ status: string; updatedAttributes: number }> {
const { attributes, mode = 'merge' } = params;
@ -145,257 +119,6 @@ export class MemoryService {
return { status: 'success' };
}
// ========== TaskNode Chain ==========
async createTaskNode(params: TaskNodeCreateParams): Promise<{ node_id: number; chain_linked: boolean; archived_path: string | undefined }> {
const result = await this.taskStore.createTaskNode(params);
// Also archive raw context if provided
let archivedPath: string | undefined = undefined;
if (params.raw_context) {
archivedPath = path.join(this.contextArchiveDir, `${params.session_id}_turn${params.turn_id}_raw.txt`);
fs.writeFileSync(archivedPath, params.raw_context, 'utf-8');
}
return { ...result, archived_path: archivedPath };
}
async getRecentTaskNodes(session_id: string, limit: number = 5): Promise<Array<{ id: number; turn_id: number; summary: string; key_facts: string[]; created_at: string }>> {
const nodes = await this.taskStore.getRecentTaskNodes(session_id, limit);
return nodes.map(n => ({
id: n.id,
turn_id: n.turn_id,
summary: n.summary,
key_facts: JSON.parse(n.key_facts || '[]') as string[],
created_at: n.created_at
}));
}
async getTaskChain(session_id: string, from_node_id?: number): Promise<TaskNodeChainResult> {
return this.taskStore.getTaskChain(session_id, from_node_id);
}
async readArchivedContext(session_id: string, turn_id: number): Promise<string | null> {
const archivePath = path.join(this.contextArchiveDir, `${session_id}_turn${turn_id}_raw.txt`);
if (fs.existsSync(archivePath)) {
return fs.readFileSync(archivePath, 'utf-8');
}
return null;
}
// ========== Context Rewrite ==========
async contextRewrite(params: ContextRewriteParams): Promise<ContextRewriteResult> {
const { context, maxEntities = 20, summary } = params;
// 1. 提取关键句子
const sentences = context
.split(/[。!?\n]+/)
.map(s => s.trim())
.filter(s => s.length > 5 && s.length < 200);
// 2. 提取实体(使用增强规则)
const entityPattern = /(?:我|你|用户|AI|系统|项目|任务|文件|代码|程序|功能|接口|类|方法|变量|数据库|服务器|客户端|前端|后端|API|Web|App|Python|JavaScript|TypeScript|Java|Go|Rust|C\+\+|数据库|图|记忆|插件|工具|技能|记忆|上下文|偏好|习惯|决策|重要|关键|目标|计划|问题|解决|方案|结果|选择|决定|配置|环境|版本|分支|提交|合并|发布|部署|测试|调试|优化|重构|设计|架构|模式|框架|库|包|依赖|构建|编译|运行|执行|输出|输入|错误|异常|警告|日志|监控|性能|安全|权限|认证|授权|缓存|队列|消息|事件|状态|数据|模型|视图|控制器|路由|请求|响应|协议|格式|编码|解析|序列化|反序列化|同步|异步|并行|并发|线程|进程|阻塞|非阻塞|流|管道|过滤|映射|归约|排序|搜索|匹配|替换|分割|合并|压缩|解压|加密|解密|签名|验证|哈希|随机|唯一|索引|主键|外键|约束|事务|回滚|提交|锁|死锁|超时|重试|降级|熔断|限流|负载|均衡|路由|网关|代理|转发|重写|镜像|快照|备份|恢复|复制|分片|分区|集群|节点|拓扑|网络|域名|IP|端口|套接字|连接|会话|Cookie|Token|JWT|OAuth|SSO|LDAP|AD|Kerberos|证书|CA|TLS|SSL|HTTPS|HTTP|TCP|UDP|WebSocket|gRPC|REST|GraphQL|SOAP|XML|JSON|YAML|TOML|INI|CSV|TSV|Markdown|HTML|CSS|Sass|Less|Stylus|PostCSS|Tailwind|Bootstrap|jQuery|React|Vue|Angular|Svelte|Next|Nuxt|Express|Koa|Fastify|Nest|Django|Flask|FastAPI|Tornado|Spring|Laravel|Rails|Sinatra|Phoenix|Lumen|CodeIgniter|Symfony|Zend|Cake|Fuel|Yii|Phalcon|Slim|Mezzio|Laminas|Expressive|Struts|JSF|GWT|Vaadin|Wicket|Play|Akka|Vert|Quarkus|Micronaut|Helidon|Ktor|http4k|Javalin|Spark|Dropwizard|SpringBoot|Micronaut|Quarkus|Helidon|Ktor|http4k|Javalin|Spark|Dropwizard|Guice|Dagger|Spring|CDI|OSGi|EJB|JPA|Hibernate|MyBatis|EclipseLink|OpenJPA|DataNucleus|ObjectDB|Versant|db4o|NeoDatis|Perst|H2|SQLite|MySQL|PostgreSQL|Oracle|SQLServer|DB2|Sybase|Informix|Teradata|Vertica|Greenplum|Redshift|BigQuery|Snowflake|Databricks|SparkSQL|Hive|Impala|Presto|Trino|Drill|Phoenix|HBase|Cassandra|MongoDB|CouchDB|DynamoDB|DocumentDB|Firestore|CosmosDB|Redis|Memcached|Riak|Voldemort|Couchbase|Aerospike|Scylla| Yugabyte|TiDB|Cockroach|Vitess|ProxySQL|MaxScale|PgBouncer|Odyssey| Pgpool|Slony|Bucardo|Londiste|Skytools|WalE|Barman|PgBackRest|PgDump| PgRestore|PgUpgrade|PgAdmin|PgStudio|OmniDB|DBeaver|Navicat|DataGrip| TablePlus|SequelPro|HeidiSQL|MySQLWorkbench|phpMyAdmin|Adminer|SQLBuddy| Chive|TinyTinyRSS|FreshRSS|Miniflux|Stringer|Feedly|Inoreader|NewsBlur| TheOldReader|CommaFeed|BazQux|Feedbin|Feed Wrangler|FeedHQ|FeedReader| Liferea|QuiteRSS|RSSOwl|Thunderbird|Outlook|AppleMail|Spark|Airmail| Newton|Canary|Edison|BlueMail|TypeApp|Nine|K9|FairEmail|Aquamail| ProtonMail|Tutanota|CTemplar|StartMail|Runbox|CounterMail|Hushmail| KolabNow|Mailbox.org|Posteo|Soverin|TheXYZ|ZohoMail|FastMail|GandiMail| Namecheap|Hover|DreamHost|HostGator|Bluehost|GoDaddy|Namecheap|Dynadot| GoogleDomains|CloudflareRegistrar|Route53|DNSimple|Gandi|OVH|Hetzner| Linode|DigitalOcean|Vultr|UpCloud|Scaleway|Exoscale|CherryServers| Packet|Equinix|AWS|Azure|GCP|IBMCloud|OracleCloud|AlibabaCloud|TencentCloud| HuaweiCloud|BaiduCloud|JDCloud|UCloud|QingCloud|ChinaTelecom|ChinaUnicom| ChinaMobile|GreatWall|DrPeng|Broadnet|Wasu|Born|Topway|Guangdong| Guangxi|Hainan|Chongqing|Sichuan|Guizhou|Yunnan|Xizang|Shaanxi| Gansu|Qinghai|Ningxia|Xinjiang|Beijing|Tianjin|Hebei|Shanxi|InnerMongolia| Liaoning|Jilin|Heilongjiang|Shanghai|Jiangsu|Zhejiang|Anhui|Fujian| Jiangxi|Shandong|Henan|Hubei|Hunan|Guangdong|Guangxi|Hainan|Chongqing| Sichuan|Guizhou|Yunnan|Xizang|Shaanxi|Gansu|Qinghai|Ningxia|Xinjiang| HongKong|Macau|Taiwan)/g;
const foundEntities = new Set<string>();
sentences.forEach(s => {
const matches = s.match(entityPattern);
if (matches) matches.forEach(m => foundEntities.add(m));
});
if (foundEntities.size < 3) {
const words = context.split(/\s+/).filter(w => w.length >= 2 && w.length <= 20);
const freq = new Map<string, number>();
words.forEach(w => freq.set(w, (freq.get(w) || 0) + 1));
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
sorted.slice(0, maxEntities).forEach(([w]) => foundEntities.add(w));
}
const entities = Array.from(foundEntities).slice(0, maxEntities);
// 3. 生成摘要
const keySentences = sentences
.filter(s => entities.some(e => s.includes(e)))
.slice(0, 5);
const generatedSummary = summary || keySentences.join('') || context.slice(0, 200);
// 4. 生成三元组关系
const triplets: Array<{ subject: string; relation: string; object: string; confidence?: number }> = [];
// 实体共现关系
for (let i = 0; i < Math.min(entities.length, 10); i++) {
for (let j = i + 1; j < Math.min(entities.length, 10); j++) {
const s1 = entities[i];
const s2 = entities[j];
const coOccur = sentences.some(s => s.includes(s1) && s.includes(s2));
if (coOccur) {
triplets.push({
subject: s1,
relation: '关联',
object: s2,
confidence: 0.7
});
}
}
}
// 检测偏好和决策模式
const preferencePatterns = [
{ pattern: /喜欢|偏好|爱好|倾向|习惯|常用|总是|经常/, relation: '偏好' },
{ pattern: /决定|决策|选择|确定|定了|采用|使用|方案/, relation: '决策' },
{ pattern: /重要|关键|核心|主要|首要|必须|务必|一定/, relation: '重要性' },
{ pattern: /目标|计划|打算|准备|预计|期望|希望|想要/, relation: '意图' },
{ pattern: /问题|错误|异常|失败|困难|挑战|障碍|风险/, relation: '问题' },
{ pattern: /解决|修复|处理|应对|克服|消除|避免|预防/, relation: '解决方案' }
];
sentences.forEach(sentence => {
preferencePatterns.forEach(({ pattern, relation }) => {
if (pattern.test(sentence)) {
const matchedEntities = entities.filter(e => sentence.includes(e));
if (matchedEntities.length > 0) {
triplets.push({
subject: matchedEntities[0],
relation,
object: sentence.slice(0, 100),
confidence: 0.85
});
}
}
});
});
// 创建摘要节点
const summaryId = `Summary_${Date.now()}`;
triplets.push({
subject: summaryId,
relation: 'is_type',
object: 'ContextSummary',
confidence: 1.0
});
triplets.push({
subject: summaryId,
relation: 'HAS_CONTENT',
object: generatedSummary.slice(0, 500),
confidence: 1.0
});
triplets.push({
subject: summaryId,
relation: 'SOURCE_TYPE',
object: 'context_rewrite',
confidence: 1.0
});
// 实体与摘要的关联
entities.slice(0, 5).forEach(e => {
triplets.push({
subject: summaryId,
relation: 'MENTIONS',
object: e,
confidence: 0.8
});
});
// 写入记忆图
const commitResult = await this.db.commit({ triplets });
// 同时存入任务节点表
const keyFacts = entities.slice(0, 10).map(e => `实体: ${e}`);
keyFacts.push(`摘要: ${generatedSummary.slice(0, 100)}`);
await this.taskStore.createTaskNode({
session_id: this.db.getSessionId(),
turn_id: Date.now(),
summary: generatedSummary,
key_facts: keyFacts,
raw_context: context
});
return {
extractedEntities: entities.length,
extractedRelations: commitResult.createdRelations,
summary: generatedSummary,
compressed: context.length > generatedSummary.length
};
}
// ========== Working Memory Chain ==========
async workingMemoryChain(params: WorkingMemoryChainParams = {}): Promise<WorkingMemoryChainResult> {
const { maxDepth = 3, recentOnly = true } = params;
// 使用 TaskNode 链获取工作记忆
const sessionId = this.db.getSessionId();
const recentNodes = await this.taskStore.getRecentTaskNodes(sessionId, maxDepth * 3);
const chain = recentNodes.map(n => ({
subject: `Turn_${n.turn_id}`,
relation: 'summary',
object: n.summary.slice(0, 100),
timestamp: n.created_at
}));
// 同时从图数据库获取活跃关系补充
const timeFilter = recentOnly ? { days: 1 } : undefined;
const graphResult = await this.db.recall({
queryIntent: '',
seedEntities: [],
depth: maxDepth,
timeRange: timeFilter,
sessionFilter: sessionId
});
const graphChain = graphResult.relations
.filter(r => r.status === 'active')
.slice(0, 10)
.map(r => {
const source = graphResult.entities.find(e => e.id === r.sourceId);
const target = graphResult.entities.find(e => e.id === r.targetId);
return {
subject: source?.name || r.sourceId,
relation: r.relationType,
object: target?.name || r.targetId,
timestamp: r.createdAt.toISOString()
};
});
return {
chain: [...chain, ...graphChain].slice(0, 20),
entityCount: graphResult.entities.length + recentNodes.length
};
}
async storeSemanticMemory(text: string, source?: string, sourceLine?: number): Promise<string> {
const id = this.generateId();
const semanticSearch = this.db.getSemanticSearch();
const embedding = await semanticSearch.generateEmbedding(text);
semanticSearch.storeEmbedding(id, text, embedding, source, sourceLine);
return id;
}
async semanticSearch(query: string, limit: number = 10): Promise<Array<{ id: string; text: string; source?: string; similarity: number }>> {
const semanticSearch = this.db.getSemanticSearch();
const queryEmbedding = await semanticSearch.generateEmbedding(query);
const results = semanticSearch.searchSimilar(queryEmbedding, limit);
return results.map(r => ({
id: r.id,
text: r.text,
source: r.source || undefined,
similarity: Math.round(r.similarity * 1000) / 1000
}));
}
async readMemoryFragment(path: string, fromLine?: number, lines?: number): Promise<string> {
const content = fs.readFileSync(path, 'utf-8');
if (fromLine !== undefined && lines !== undefined) {
const allLines = content.split('\n');
return allLines.slice(fromLine - 1, fromLine - 1 + lines).join('\n');
}
return content;
}
// ========== Utility ==========
setSessionId(sessionId: string): void {
this.db.setSessionId(sessionId);
}
@ -403,8 +126,4 @@ export class MemoryService {
getSessionId(): string {
return this.db.getSessionId();
}
private generateId(): string {
return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
}
}

View File

@ -1,86 +0,0 @@
import Database from 'better-sqlite3';
export interface SemanticMemory {
id: string;
text: string;
embedding: Float32Array;
source: string;
sourceLine?: number;
createdAt: Date;
}
export interface SearchResult {
id: string;
text: string;
source: string;
sourceLine?: number;
similarity: number;
}
export class SemanticSearchEngine {
private db: Database.Database;
private embeddingModel: any;
private modelReady: boolean = false;
constructor(db: Database.Database) {
this.db = db;
this.initialize();
}
private initialize(): void {
this.db.exec(`
CREATE TABLE IF NOT EXISTS embeddings (
id TEXT PRIMARY KEY,
text TEXT NOT NULL,
embedding BLOB NOT NULL,
source TEXT,
source_line INTEGER,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_embeddings_source ON embeddings(source);
`);
}
async loadModel(): Promise<void> {
if (this.modelReady) return;
const { pipeline } = await import('@xenova/transformers') as any;
this.embeddingModel = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
this.modelReady = true;
}
async generateEmbedding(text: string): Promise<Float32Array> {
await this.loadModel();
const output = await this.embeddingModel(text, { pooling: 'mean', normalize: true });
return new Float32Array(output.data);
}
storeEmbedding(id: string, text: string, embedding: Float32Array, source?: string, sourceLine?: number): void {
const stmt = this.db.prepare(`
INSERT OR REPLACE INTO embeddings (id, text, embedding, source, source_line)
VALUES (?, ?, ?, ?, ?)
`);
stmt.run(id, text, Buffer.from(embedding.buffer), source || null, sourceLine || null);
}
searchSimilar(queryEmbedding: Float32Array, limit: number = 10): SearchResult[] {
const rows = this.db.prepare('SELECT id, text, embedding, source, source_line FROM embeddings').all();
const results = rows.map((row: any) => ({
id: row.id,
text: row.text,
source: row.source,
sourceLine: row.source_line,
similarity: cosineSimilarity(queryEmbedding, new Float32Array(row.embedding.buffer))
}));
return results.sort((a: SearchResult, b: SearchResult) => b.similarity - a.similarity).slice(0, limit);
}
}
export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}

View File

@ -1,183 +0,0 @@
import Database from 'better-sqlite3';
import * as fs from 'fs';
import * as path from 'path';
import type { TaskNodeData } from './types.js';
export { TaskNodeData as TaskNode };
export class TaskNodeStore {
private db: Database.Database;
private archiveDir: string;
constructor(dbPath?: string, archiveDir?: string) {
this.db = new Database(dbPath || 'graph_memory.db');
this.archiveDir = archiveDir || './task_archive';
this.initialize();
}
private initialize(): void {
this.db.exec(`
CREATE TABLE IF NOT EXISTS task_nodes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
turn_id INTEGER NOT NULL,
summary TEXT NOT NULL,
key_facts TEXT NOT NULL DEFAULT '[]',
created_at TEXT DEFAULT (datetime('now'))
)
`);
this.db.exec(`
CREATE TABLE IF NOT EXISTS task_chains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
from_node_id INTEGER NOT NULL,
to_node_id INTEGER NOT NULL,
relation_type TEXT DEFAULT 'next',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (from_node_id) REFERENCES task_nodes(id),
FOREIGN KEY (to_node_id) REFERENCES task_nodes(id)
)
`);
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_task_nodes_session ON task_nodes(session_id);
CREATE INDEX IF NOT EXISTS idx_task_nodes_turn ON task_nodes(session_id, turn_id);
CREATE INDEX IF NOT EXISTS idx_task_chains_session ON task_chains(session_id);
CREATE INDEX IF NOT EXISTS idx_task_chains_from ON task_chains(from_node_id);
`);
if (!fs.existsSync(this.archiveDir)) {
fs.mkdirSync(this.archiveDir, { recursive: true });
}
}
async createTaskNode(params: {
session_id: string;
turn_id: number;
summary: string;
key_facts: string[];
raw_context?: string | undefined;
}): Promise<{ node_id: number; chain_linked: boolean }> {
const { session_id, turn_id, summary, key_facts, raw_context } = params;
const insert = this.db.prepare(`
INSERT INTO task_nodes (session_id, turn_id, summary, key_facts)
VALUES (?, ?, ?, ?)
`);
const result = insert.run(session_id, turn_id, summary, JSON.stringify(key_facts));
const nodeId = Number(result.lastInsertRowid);
// Link to previous node in chain
let chainLinked = false;
const prevNode = this.db.prepare(`
SELECT id FROM task_nodes
WHERE session_id = ? AND turn_id < ?
ORDER BY turn_id DESC LIMIT 1
`).get(session_id, turn_id) as { id: number } | undefined;
if (prevNode) {
this.db.prepare(`
INSERT INTO task_chains (session_id, from_node_id, to_node_id, relation_type)
VALUES (?, ?, ?, 'next')
`).run(session_id, prevNode.id, nodeId);
chainLinked = true;
}
// Archive raw context to text file if provided
if (raw_context) {
const archivePath = path.join(this.archiveDir, `${session_id}_turn${turn_id}.txt`);
fs.writeFileSync(archivePath, raw_context, 'utf-8');
}
return { node_id: nodeId, chain_linked: chainLinked };
}
async getRecentTaskNodes(session_id: string, limit: number = 5): Promise<TaskNodeData[]> {
const rows = this.db.prepare(`
SELECT * FROM task_nodes
WHERE session_id = ?
ORDER BY turn_id DESC
LIMIT ?
`).all(session_id, limit) as Array<Record<string, unknown>>;
return rows.map(r => ({
id: r.id as number,
session_id: r.session_id as string,
turn_id: r.turn_id as number,
summary: r.summary as string,
key_facts: r.key_facts as string,
created_at: r.created_at as string
})).reverse(); // Return in chronological order
}
async getTaskChain(session_id: string, from_node_id?: number): Promise<{
nodes: TaskNodeData[];
relations: Array<{ from: number; to: number; type: string }>;
}> {
let startNode = from_node_id;
if (!startNode) {
const latest = this.db.prepare(`
SELECT id FROM task_nodes WHERE session_id = ? ORDER BY turn_id DESC LIMIT 1
`).get(session_id) as { id: number } | undefined;
if (!latest) return { nodes: [], relations: [] };
startNode = latest.id;
}
// Walk backwards through the chain
const nodes: TaskNodeData[] = [];
const relations: Array<{ from: number; to: number; type: string }> = [];
const visited = new Set<number>();
let current = startNode;
while (current && !visited.has(current)) {
visited.add(current);
const node = this.db.prepare(`SELECT * FROM task_nodes WHERE id = ?`).get(current) as Record<string, unknown> | undefined;
if (node) {
nodes.unshift({
id: node.id as number,
session_id: node.session_id as string,
turn_id: node.turn_id as number,
summary: node.summary as string,
key_facts: node.key_facts as string,
created_at: node.created_at as string
});
}
const prevChain = this.db.prepare(`
SELECT from_node_id, relation_type FROM task_chains WHERE to_node_id = ? AND session_id = ?
`).get(current, session_id) as { from_node_id: number; relation_type: string } | undefined;
if (prevChain) {
relations.unshift({
from: prevChain.from_node_id,
to: current,
type: prevChain.relation_type
});
current = prevChain.from_node_id;
} else {
break;
}
}
return { nodes, relations };
}
async archiveRawContext(session_id: string, turn_id: number, raw_context: string): Promise<string> {
const archivePath = path.join(this.archiveDir, `${session_id}_turn${turn_id}.txt`);
fs.writeFileSync(archivePath, raw_context, 'utf-8');
return archivePath;
}
async readArchivedContext(session_id: string, turn_id: number): Promise<string | null> {
const archivePath = path.join(this.archiveDir, `${session_id}_turn${turn_id}.txt`);
if (fs.existsSync(archivePath)) {
return fs.readFileSync(archivePath, 'utf-8');
}
return null;
}
close(): void {
this.db.close();
}
}

View File

@ -5,10 +5,9 @@ export interface Entity {
mentionCount: number;
createdAt: Date;
updatedAt: Date;
depth?: number; // BFS 搜索深度标注
}
export const __types = true;
export type RelationStatus = 'active' | 'deleted' | 'archived' | 'superseded';
export interface Relation {
@ -23,6 +22,7 @@ export interface Relation {
createdAt: Date;
updatedAt: Date;
dateBucket: string;
depth?: number; // BFS 搜索深度标注
}
export interface Triplet {
@ -73,6 +73,7 @@ export interface CommitResult {
export interface PurgeResult {
deleted: number;
mode: string;
message?: string;
}
export type TaskState = '进行中' | '已完成' | '已暂停' | '已取消';
@ -125,48 +126,3 @@ export interface TaskLinkInfoParams {
task_id: string;
info_node: string;
}
export interface ContextRewriteParams {
context: string;
maxEntities?: number | undefined;
summary?: string | undefined;
}
export interface ContextRewriteResult {
extractedEntities: number;
extractedRelations: number;
summary: string;
compressed: boolean;
}
export interface WorkingMemoryChainParams {
maxDepth?: number | undefined;
recentOnly?: boolean | undefined;
}
export interface WorkingMemoryChainResult {
chain: Array<{ subject: string; relation: string; object: string; timestamp: string }>;
entityCount: number;
}
export interface TaskNodeData {
id: number;
session_id: string;
turn_id: number;
summary: string;
key_facts: string;
created_at: string;
}
export interface TaskNodeCreateParams {
session_id: string;
turn_id: number;
summary: string;
key_facts: string[];
raw_context?: string | undefined;
}
export interface TaskNodeChainResult {
nodes: TaskNodeData[];
relations: Array<{ from: number; to: number; type: string }>;
}

View File

@ -1,466 +1,187 @@
import { Type } from '@sinclair/typebox';
import type { Static } from '@sinclair/typebox';
import { GraphDatabase } from '../../graph_memory/graph_database.js';
import { MemoryService } from '../../graph_memory/memory_service.js';
import { ToolLimiter } from '../tool_limiter.js';
import type { Tool, ToolCategory, PermissionLevel, ToolInputSchema, ToolOutput, ToolInput, ToolExecutionContext } from 'waterflow-ts/dist/runtime/core/tools/tool_interface.js';
import { GraphDatabase } from '../../graph_memory/graph_database';
import { MemoryService } from '../../graph_memory/memory_service';
export const GraphMemoryToolSchema = Type.Object({
action: Type.String({
description: '记忆操作类型',
enum: [
'recall', 'commit', 'purge', 'introspect', 'archive', 'cleanup',
'persona_update', 'persona_clear',
'task_create', 'task_set_state', 'task_delete', 'task_link_info',
'context_rewrite', 'working_memory_chain',
'task_node_create', 'task_node_get_recent', 'task_node_get_chain',
'memory_search', 'memory_get'
]
}),
params: Type.Object({
queryIntent: Type.Optional(Type.String({ description: '搜索意图' })),
seedEntities: Type.Optional(Type.Array(Type.String({ description: '实体' }), { description: '种子实体' })),
depth: Type.Optional(Type.Number({ description: '检索深度' })),
sessionFilter: Type.Optional(Type.String({ description: '会话ID过滤' })),
triplets: Type.Optional(Type.Array(
Type.Object({
subject: Type.String({ description: '主体' }),
relation: Type.String({ description: '关系' }),
object: Type.String({ description: '客体' }),
confidence: Type.Optional(Type.Number({ description: '置信度' }))
}, { description: '三元组' }),
{ description: '三元组数组' }
)),
sessionId: Type.Optional(Type.String({ description: '会话ID' })),
turnId: Type.Optional(Type.Number({ description: '轮次ID' })),
criteria: Type.Optional(Type.Object({
subject: Type.Optional(Type.String({ description: '主体' })),
target: Type.Optional(Type.String({ description: '客体' })),
relation: Type.Optional(Type.String({ description: '关系' })),
sessionId: Type.Optional(Type.String({ description: '会话ID' }))
}, { description: '删除条件' })),
mode: Type.Optional(Type.String({
enum: ['soft', 'hard', 'supersede'],
description: '删除模式'
})),
newRelation: Type.Optional(Type.Object({
relation: Type.String({ description: '关系' }),
target: Type.String({ description: '客体' })
}, { description: '新关系supersede模式' })),
attributes: Type.Optional(Type.Array(
Type.Object({
attribute: Type.String({ description: '属性名' }),
value: Type.String({ description: '属性值' })
}, { description: '属性' }),
{ description: '属性数组' }
)),
confirm: Type.Optional(Type.Boolean({ description: '确认清除' })),
task_id: Type.Optional(Type.String({ description: '任务ID' })),
description: Type.Optional(Type.String({ description: '任务描述' })),
state: Type.Optional(Type.String({ description: '任务状态' })),
info_nodes: Type.Optional(Type.Array(Type.String({ description: '节点' }), { description: '信息节点' })),
info_node: Type.Optional(Type.String({ description: '信息节点' })),
days: Type.Optional(Type.Number({ description: '归档天数' })),
dry_run: Type.Optional(Type.Boolean({ description: '仅预览不删除' })),
context: Type.Optional(Type.String({ description: '要压缩的上下文文本' })),
maxEntities: Type.Optional(Type.Number({ description: '最大实体数' })),
summary: Type.Optional(Type.String({ description: '自定义摘要' })),
recentOnly: Type.Optional(Type.Boolean({ description: '仅最近' })),
maxDepth: Type.Optional(Type.Number({ description: '最大深度' })),
session_id: Type.Optional(Type.String({ description: '会话IDTaskNode用' })),
turn_id: Type.Optional(Type.Number({ description: '轮次ID' })),
key_facts: Type.Optional(Type.Array(Type.String({ description: '关键事实' }), { description: '关键事实数组' })),
raw_context: Type.Optional(Type.String({ description: '原始上下文(存档用)' })),
limit: Type.Optional(Type.Number({ description: '限制数量' })),
from_node_id: Type.Optional(Type.Number({ description: '起始节点ID' })),
query: Type.Optional(Type.String({ description: '语义搜索查询' })),
corpus: Type.Optional(Type.String({
enum: ['memory', 'wiki', 'all'],
description: '搜索语料范围'
})),
path: Type.Optional(Type.String({ description: '记忆文件路径' })),
fromLine: Type.Optional(Type.Number({ description: '起始行号' })),
lines: Type.Optional(Type.Number({ description: '读取行数' }))
}, { description: '操作参数' })
});
const GRAPH_MEMORY_TOOL_ID = 'builtin:graph_memory';
const GRAPH_MEMORY_TOOL_API_NAME = 'graph_memory'; // API 兼容名称(不含冒号,符合 ^[a-zA-Z0-9_-]+$ 要求)
export type GraphMemoryToolParams = Static<typeof GraphMemoryToolSchema>;
/**
* 工具名称映射工具 - 用于处理 API 对工具名称格式的限制
* OpenAI/DeepSeek API 要求工具名称符合 ^[a-zA-Z0-9_-]+$ 正则表达式
* 而 TrulyMEM 的内部 ID 使用 "builtin:xxx" 格式(含冒号)
*/
const GRAPH_MEMORY_TOOL_DESCRIPTION = `图记忆工具 - 让 AI 拥有真正的长期记忆能力。作为 OpenClaw memory-core 的增强补充,不替代其核心功能。
/**
* 将内部工具 ID 映射为 API 兼容名称
* @param toolId 内部工具 ID如 "builtin:graph_memory"
* @returns API 兼容名称,如 "graph_memory"
*/
export function mapToolIdToApiName(toolId: string): string {
// 移除 "builtin:" 前缀
if (toolId.startsWith('builtin:')) {
return toolId.slice(8);
}
// 其他前缀也移除(如 "mcp:", "plugin:"
const colonIndex = toolId.indexOf(':');
if (colonIndex > 0) {
return toolId.slice(colonIndex + 1);
}
return toolId;
}
/**
* 将 API 返回的工具名称映射回内部 ID
* @param apiName API 返回的工具名称,如 "graph_memory"
* @param prefix 内部 ID 前缀,默认 "builtin:"
* @returns 内部工具 ID如 "builtin:graph_memory"
*/
export function mapApiNameToToolId(apiName: string, prefix = 'builtin:'): string {
// 如果已经是完整 ID 格式,直接返回
if (apiName.includes(':')) {
return apiName;
}
return `${prefix}${apiName}`;
}
const GRAPH_MEMORY_TOOL_DESCRIPTION = `图记忆工具 - 让 AI 拥有真正的长期记忆能力
操作:
- recall: 检索记忆(可选参数: queryIntent, seedEntities, depth, sessionFilter
- commit: 写入记忆(必需参数: triplets
- purge: 删除记忆(可选参数: criteria, mode, newRelation
- introspect: 查看状态无参数
- archive: 归档旧记忆(可选参数: days, 默认30天
- cleanup: 清理无效数据(可选参数: dry_run, 默认true预览模式
- persona_update: 更新人设(必需参数: attributes; 可选: mode=merge/replace
- persona_clear: 清除人设(必需参数: confirm=true
- task_create: 创建任务(必需参数: task_id, description; 可选: info_nodes
- task_set_state: 设置任务状态(必需参数: task_id, state
- task_delete: 删除任务(必需参数: task_id
- task_link_info: 关联信息(必需参数: task_id, info_node
- context_rewrite: 压缩上下文为关键记忆(必需参数: context; 可选: maxEntities, summary
- working_memory_chain: 获取工作记忆链(可选参数: maxDepth, recentOnly
- memory_search: 语义向量搜索(必需参数: query; 可选: limit, corpus
- memory_get: 精确读取记忆文件片段(必需参数: path; 可选: fromLine, lines`;
- recall: 检索记忆 - 提供 queryIntent (搜索意图) 和可选的 seedEntities
- commit: 写入记忆 - 必须使用 triplets 数组格式,每个三元组包含 subject, relation, object
- purge: 删除记忆 - 提供 criteria 指定删除条件
- introspect: 查看状态 - 无参数
- persona_update/clear: 人设管理
- task_create/set_state/delete: 任务管理
// ==================== 参数验证 ====================
【commit 操作的三元组格式】
triplets 必须是数组,每个元素是 {subject, relation, object, confidence} 格式:
示例: {"action":"commit","params":{"triplets":[{"subject":"Alice","relation":"is a","object":"engineer","confidence":0.95}]}}
- subject: 实体名称 (如用户名、技术名称)
- relation: 关系描述 (如 "is a", "likes", "knows")
- object: 目标实体 (如职业、爱好、技术)
- confidence: 置信度 0-1 (可选默认0.9)
interface ValidationError {
field: string;
message: string;
}
【recall 操作】
示例: {"action":"recall","params":{"queryIntent":"用户的学习偏好","seedEntities":["Alice"]}}`;
function validateRecallParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
const queryIntent = params.queryIntent;
const seedEntities = params.seedEntities;
export class GraphMemoryTool implements Tool {
readonly id = GRAPH_MEMORY_TOOL_ID;
readonly name = 'GraphMemory';
readonly apiName = GRAPH_MEMORY_TOOL_API_NAME;
readonly description = GRAPH_MEMORY_TOOL_DESCRIPTION;
readonly category: ToolCategory = 'analysis';
readonly permissionLevel: PermissionLevel = 'safe';
readonly alwaysLoad = true;
if ((!queryIntent || (typeof queryIntent === 'string' && queryIntent.trim() === '')) &&
(!seedEntities || !Array.isArray(seedEntities) || seedEntities.length === 0)) {
errors.push({ field: 'queryIntent/seedEntities', message: 'recall 操作需要提供 queryIntent 或 seedEntities 之一' });
}
if (params.depth !== undefined) {
const depth = Number(params.depth);
if (isNaN(depth) || depth < 1 || depth > 5) {
errors.push({ field: 'depth', message: 'depth 必须在 1-5 之间' });
}
}
return errors;
}
function validateCommitParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
const triplets = params.triplets;
if (!triplets || !Array.isArray(triplets) || triplets.length === 0) {
errors.push({ field: 'triplets', message: 'commit 操作必需提供 triplets 数组' });
return errors;
}
for (let i = 0; i < triplets.length; i++) {
const t = triplets[i] as Record<string, unknown>;
if (!t.subject || typeof t.subject !== 'string' || t.subject.trim() === '') {
errors.push({ field: `triplets[${i}].subject`, message: '三元组主体不能为空字符串' });
}
if (!t.relation || typeof t.relation !== 'string' || t.relation.trim() === '') {
errors.push({ field: `triplets[${i}].relation`, message: '三元组关系不能为空字符串' });
}
if (!t.object || typeof t.object !== 'string' || t.object.trim() === '') {
errors.push({ field: `triplets[${i}].object`, message: '三元组客体不能为空字符串' });
}
if (t.confidence !== undefined) {
const c = Number(t.confidence);
if (isNaN(c) || c < 0 || c > 1) {
errors.push({ field: `triplets[${i}].confidence`, message: '置信度必须在 0-1 之间' });
readonly inputSchema: ToolInputSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: [
'recall', 'commit', 'purge', 'introspect',
'persona_update', 'persona_clear',
'task_create', 'task_set_state', 'task_delete', 'task_link_info'
],
description: '记忆操作类型'
},
params: {
type: 'object',
description: '操作参数',
properties: {
queryIntent: { type: 'string', description: '搜索意图' },
seedEntities: { type: 'array', items: { type: 'string', description: '实体' }, description: '种子实体' },
depth: { type: 'number', description: '检索深度' },
sessionFilter: { type: 'string', description: '会话ID过滤' },
triplets: {
type: 'array',
description: '知识三元组数组。每个三元组描述 subject-relation-object 关系,用于存储记忆知识。',
items: {
type: 'object',
description: '三元组: {subject, relation, object, confidence} - subject/relation/object 必填',
properties: {
subject: { type: 'string', description: '主体实体,如人名、技术名称等' },
relation: { type: 'string', description: '关系描述,如 "is a", "likes", "knows", "uses" 等' },
object: { type: 'string', description: '客体实体,如职业、爱好、技术名称等' },
confidence: { type: 'number', description: '置信度 (0-1),默认 0.9', default: 0.9 }
}
}
},
sessionId: { type: 'string', description: '会话ID' },
turnId: { type: 'number', description: '轮次ID' },
criteria: {
type: 'object',
properties: {
subject: { type: 'string', description: '主体' },
target: { type: 'string', description: '客体' },
relation: { type: 'string', description: '关系' },
sessionId: { type: 'string', description: '会话ID' }
},
description: '删除条件'
},
mode: { type: 'string', enum: ['soft', 'hard', 'supersede'], description: '删除模式' },
attributes: {
type: 'array',
items: {
type: 'object',
description: '属性',
properties: {
attribute: { type: 'string', description: '属性名' },
value: { type: 'string', description: '属性值' }
}
},
description: '属性数组'
},
confirm: { type: 'boolean', description: '确认清除' },
task_id: { type: 'string', description: '任务ID' },
description: { type: 'string', description: '任务描述' },
state: { type: 'string', description: '任务状态' },
info_nodes: { type: 'array', items: { type: 'string', description: '节点' }, description: '信息节点' },
info_node: { type: 'string', description: '信息节点' }
}
}
},
required: ['action', 'params']
};
private db: GraphDatabase;
private service: MemoryService;
constructor(sessionId?: string) {
this.db = new GraphDatabase(sessionId);
this.service = new MemoryService(this.db);
}
async handler(params: ToolInput, context: ToolExecutionContext): Promise<ToolOutput> {
if (context.abortController?.signal?.aborted) {
throw new Error('Operation aborted');
}
const action = params.action as string;
const actionParams = params.params as Record<string, unknown>;
const logger = context?.logger;
try {
logger?.info(`[GraphMemoryTool] Executing action: ${action}`);
const result = await this.executeAction(action, actionParams);
logger?.info(`[GraphMemoryTool] Action ${action} completed successfully`);
return JSON.stringify({ success: true, data: result }, null, 2);
} catch (error) {
logger?.error(`[GraphMemoryTool] Action ${action} failed:`, error);
return JSON.stringify({
success: false,
error: {
type: 'execution_error',
message: error instanceof Error ? error.message : String(error)
}
}, null, 2);
}
}
return errors;
}
function validatePurgeParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (params.mode === 'supersede' && (!params.newRelation || typeof params.newRelation !== 'object')) {
errors.push({ field: 'newRelation', message: 'supersede 模式必需提供 newRelation' });
}
if (params.newRelation) {
const nr = params.newRelation as Record<string, unknown>;
if (!nr.relation || typeof nr.relation !== 'string') {
errors.push({ field: 'newRelation.relation', message: 'newRelation.relation 必须是字符串' });
}
if (!nr.target || typeof nr.target !== 'string') {
errors.push({ field: 'newRelation.target', message: 'newRelation.target 必须是字符串' });
}
}
return errors;
}
function validatePersonaUpdateParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
const attributes = params.attributes;
if (!attributes || !Array.isArray(attributes) || attributes.length === 0) {
errors.push({ field: 'attributes', message: 'persona_update 操作必需提供 attributes 数组' });
return errors;
}
for (let i = 0; i < attributes.length; i++) {
const attr = attributes[i] as Record<string, unknown>;
if (!attr.attribute || typeof attr.attribute !== 'string' || attr.attribute.trim() === '') {
errors.push({ field: `attributes[${i}].attribute`, message: '属性名不能为空字符串' });
}
if (attr.value === undefined || typeof attr.value !== 'string') {
errors.push({ field: `attributes[${i}].value`, message: '属性值必须是字符串' });
}
}
return errors;
}
function validateTaskCreateParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
errors.push({ field: 'task_id', message: 'task_create 操作必需提供 task_id 字符串' });
}
if (!params.description || typeof params.description !== 'string') {
errors.push({ field: 'description', message: 'task_create 操作必需提供 description 字符串' });
}
return errors;
}
function validateTaskSetStateParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
errors.push({ field: 'task_id', message: 'task_set_state 操作必需提供 task_id 字符串' });
}
if (!params.state || typeof params.state !== 'string' || params.state.trim() === '') {
errors.push({ field: 'state', message: 'task_set_state 操作必需提供 state 字符串' });
}
return errors;
}
function validateTaskDeleteParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
errors.push({ field: 'task_id', message: 'task_delete 操作必需提供 task_id 字符串' });
}
return errors;
}
function validateTaskLinkInfoParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.task_id || typeof params.task_id !== 'string' || params.task_id.trim() === '') {
errors.push({ field: 'task_id', message: 'task_link_info 操作必需提供 task_id 字符串' });
}
if (!params.info_node || typeof params.info_node !== 'string' || params.info_node.trim() === '') {
errors.push({ field: 'info_node', message: 'task_link_info 操作必需提供 info_node 字符串' });
}
return errors;
}
function validateContextRewriteParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.context || typeof params.context !== 'string' || params.context.trim() === '') {
errors.push({ field: 'context', message: 'context_rewrite 操作必需提供 context 字符串' });
}
if (params.maxEntities !== undefined && (typeof params.maxEntities !== 'number' || params.maxEntities < 1 || params.maxEntities > 100)) {
errors.push({ field: 'maxEntities', message: 'maxEntities 必须在 1-100 之间' });
}
return errors;
}
function validateWorkingMemoryChainParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (params.maxDepth !== undefined && (typeof params.maxDepth !== 'number' || params.maxDepth < 1 || params.maxDepth > 5)) {
errors.push({ field: 'maxDepth', message: 'maxDepth 必须在 1-5 之间' });
}
return errors;
}
function validateTaskNodeCreateParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.session_id || typeof params.session_id !== 'string' || params.session_id.trim() === '') {
errors.push({ field: 'session_id', message: 'task_node_create 操作必需提供 session_id 字符串' });
}
if (params.turn_id === undefined || typeof params.turn_id !== 'number') {
errors.push({ field: 'turn_id', message: 'task_node_create 操作必需提供 turn_id 数字' });
}
if (!params.summary || typeof params.summary !== 'string' || params.summary.trim() === '') {
errors.push({ field: 'summary', message: 'task_node_create 操作必需提供 summary 字符串' });
}
if (!params.key_facts || !Array.isArray(params.key_facts) || params.key_facts.length === 0) {
errors.push({ field: 'key_facts', message: 'task_node_create 操作必需提供 key_facts 数组' });
}
return errors;
}
function validateTaskNodeGetRecentParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.session_id || typeof params.session_id !== 'string' || params.session_id.trim() === '') {
errors.push({ field: 'session_id', message: 'task_node_get_recent 操作必需提供 session_id 字符串' });
}
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1 || params.limit > 100)) {
errors.push({ field: 'limit', message: 'limit 必须在 1-100 之间' });
}
return errors;
}
function validateTaskNodeGetChainParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.session_id || typeof params.session_id !== 'string' || params.session_id.trim() === '') {
errors.push({ field: 'session_id', message: 'task_node_get_chain 操作必需提供 session_id 字符串' });
}
return errors;
}
function validatePersonaClearParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (params.confirm === undefined) {
errors.push({ field: 'confirm', message: 'persona_clear 操作必需设置 confirm: true 才能执行清除' });
}
return errors;
}
function validateMemorySearchParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.query || typeof params.query !== 'string' || params.query.trim() === '') {
errors.push({ field: 'query', message: 'memory_search 操作必需提供 query 字符串' });
}
if (params.limit !== undefined && (typeof params.limit !== 'number' || params.limit < 1 || params.limit > 50)) {
errors.push({ field: 'limit', message: 'limit 必须在 1-50 之间' });
}
return errors;
}
function validateMemoryGetParams(params: Record<string, unknown>): ValidationError[] {
const errors: ValidationError[] = [];
if (!params.path || typeof params.path !== 'string' || params.path.trim() === '') {
errors.push({ field: 'path', message: 'memory_get 操作必需提供 path 字符串' });
}
if (params.fromLine !== undefined && (typeof params.fromLine !== 'number' || params.fromLine < 1)) {
errors.push({ field: 'fromLine', message: 'fromLine 必须是正整数' });
}
if (params.lines !== undefined && (typeof params.lines !== 'number' || params.lines < 1 || params.lines > 500)) {
errors.push({ field: 'lines', message: 'lines 必须在 1-500 之间' });
}
return errors;
}
function validateParams(action: string, params: Record<string, unknown>): ValidationError[] {
switch (action) {
case 'recall': return validateRecallParams(params);
case 'commit': return validateCommitParams(params);
case 'purge': return validatePurgeParams(params);
case 'persona_update': return validatePersonaUpdateParams(params);
case 'persona_clear': return validatePersonaClearParams(params);
case 'task_create': return validateTaskCreateParams(params);
case 'task_set_state': return validateTaskSetStateParams(params);
case 'task_delete': return validateTaskDeleteParams(params);
case 'task_link_info': return validateTaskLinkInfoParams(params);
case 'context_rewrite': return validateContextRewriteParams(params);
case 'working_memory_chain': return validateWorkingMemoryChainParams(params);
case 'task_node_create': return validateTaskNodeCreateParams(params);
case 'task_node_get_recent': return validateTaskNodeGetRecentParams(params);
case 'task_node_get_chain': return validateTaskNodeGetChainParams(params);
case 'memory_search': return validateMemorySearchParams(params);
case 'memory_get': return validateMemoryGetParams(params);
default: return [];
}
}
// ==================== 日志 ====================
class GraphMemoryLogger {
private prefix = '[TrulyMEM]';
info(message: string, meta?: Record<string, unknown>): void {
console.log(`${this.prefix} [INFO] ${message}`, meta ? JSON.stringify(meta) : '');
}
warn(message: string, meta?: Record<string, unknown>): void {
console.warn(`${this.prefix} [WARN] ${message}`, meta ? JSON.stringify(meta) : '');
}
error(message: string, meta?: Record<string, unknown>): void {
console.error(`${this.prefix} [ERROR] ${message}`, meta ? JSON.stringify(meta) : '');
}
action(action: string, params: Record<string, unknown>, result?: unknown): void {
const meta: Record<string, unknown> = { action };
if (params && Object.keys(params).length > 0) {
// 敏感信息脱敏:不记录具体属性值
const sanitized = this.sanitizeParams(params);
meta.params = sanitized;
}
if (result !== undefined) {
meta.result = typeof result === 'object' && result !== null
? (result as Record<string, unknown>).success ?? 'ok'
: 'ok';
}
this.info(`action=${action}`, meta);
}
private sanitizeParams(params: Record<string, unknown>): Record<string, unknown> {
const sanitized: Record<string, unknown> = {};
for (const [key, value] of Object.entries(params)) {
if (key === 'attributes') {
sanitized[key] = Array.isArray(value)
? (value as Array<Record<string, unknown>>).map(a => a.attribute)
: value;
} else if (key === 'triplets') {
sanitized[key] = Array.isArray(value)
? `${(value as unknown[]).length} triplets`
: value;
} else {
sanitized[key] = value;
}
}
return sanitized;
}
}
// ==================== 主逻辑 ====================
export function createGraphMemoryTool(dbPath?: string, sessionId?: string) {
const db = new GraphDatabase(dbPath, sessionId);
const service = new MemoryService(db);
const limiter = new ToolLimiter();
const logger = new GraphMemoryLogger();
async function executeAction(action: string, params: Record<string, unknown>): Promise<unknown> {
// 1. 限流检查
const [allowed, reason] = limiter.canCall(action);
if (!allowed) {
logger.warn(`Rate limit blocked: ${action}`, { reason });
throw new Error(reason);
}
limiter.recordCall(action);
// 2. 参数验证
const validationErrors = validateParams(action, params);
if (validationErrors.length > 0) {
const errorMsg = validationErrors.map(e => `${e.field}: ${e.message}`).join('; ');
logger.warn(`Validation failed for ${action}`, { errors: validationErrors });
throw new Error(`参数验证失败: ${errorMsg}`);
}
logger.action(action, params);
private async executeAction(action: string, params: Record<string, unknown>): Promise<unknown> {
switch (action) {
case 'recall':
return service.recall({
return this.service.recall({
queryIntent: params.queryIntent as string || '',
seedEntities: params.seedEntities as string[] | undefined,
depth: params.depth as number | undefined,
@ -468,185 +189,62 @@ export function createGraphMemoryTool(dbPath?: string, sessionId?: string) {
});
case 'commit':
return service.commit({
return this.service.commit({
triplets: params.triplets as Array<{ subject: string; relation: string; object: string; confidence?: number }>,
sessionId: params.sessionId as string | undefined,
turnId: params.turnId as number | undefined
});
case 'purge':
return service.purge({
criteria: params.criteria as { subject?: string; target?: string; relation?: string; sessionId?: string } | undefined,
mode: params.mode as 'soft' | 'hard' | 'supersede' | undefined,
newRelation: params.newRelation as { relation: string; target: string } | undefined
return this.service.purge({
criteria: params.criteria as { subject?: string | undefined; target?: string | undefined; relation?: string | undefined; sessionId?: string | undefined } | undefined,
mode: params.mode as 'soft' | 'hard' | 'supersede' | undefined
});
case 'introspect':
return service.introspect();
return this.service.introspect();
case 'persona_update':
return service.updatePersona({
return this.service.updatePersona({
attributes: params.attributes as Array<{ attribute: string; value: string }>,
mode: params.mode as 'merge' | 'replace'
});
case 'persona_clear':
// confirm=false 时也要允许执行(返回 cancelled验证只拦截 confirm 不是 boolean 的情况
if (params.confirm === undefined) {
throw new Error('persona_clear 操作必需设置 confirm: true 才能执行清除');
}
return service.clearPersona({
return this.service.clearPersona({
confirm: params.confirm as boolean
});
case 'task_create':
return service.createTask({
return this.service.createTask({
task_id: params.task_id as string,
description: params.description as string,
info_nodes: params.info_nodes as string[] | undefined
});
case 'task_set_state':
return service.setTaskState({
return this.service.setTaskState({
task_id: params.task_id as string,
state: params.state as string
});
case 'task_delete':
return service.deleteTask({
return this.service.deleteTask({
task_id: params.task_id as string
});
case 'task_link_info':
return service.linkInfoToTask({
return this.service.linkInfoToTask({
task_id: params.task_id as string,
info_node: params.info_node as string
});
case 'context_rewrite':
return service.contextRewrite({
context: params.context as string,
maxEntities: params.maxEntities as number | undefined,
summary: params.summary as string | undefined
});
case 'working_memory_chain':
return service.workingMemoryChain({
maxDepth: params.maxDepth as number | undefined,
recentOnly: params.recentOnly as boolean | undefined
});
case 'task_node_create':
return service.createTaskNode({
session_id: params.session_id as string,
turn_id: params.turn_id as number,
summary: params.summary as string,
key_facts: params.key_facts as string[],
raw_context: params.raw_context as string | undefined
});
case 'task_node_get_recent':
return service.getRecentTaskNodes(
params.session_id as string,
params.limit as number | undefined
);
case 'task_node_get_chain':
return service.getTaskChain(
params.session_id as string,
params.from_node_id as number | undefined
);
case 'memory_search': {
const results = await service.semanticSearch(
params.query as string,
params.limit as number | undefined
);
return { results, count: results.length };
}
case 'memory_get': {
const content = await service.readMemoryFragment(
params.path as string,
params.fromLine as number | undefined,
params.lines as number | undefined
);
return { content, path: params.path };
}
case 'archive':
return service.archive(params.days as number | undefined);
case 'cleanup':
return service.cleanup(params.dry_run as boolean | undefined);
default:
throw new Error(`Unknown action: ${action}`);
}
}
return {
name: 'graph_memory',
description: GRAPH_MEMORY_TOOL_DESCRIPTION,
parameters: GraphMemoryToolSchema,
async execute(_toolCallId: string, params: Record<string, unknown>): Promise<{
content: Array<{ type: 'text'; text: string }>;
}> {
const action = params.action as string;
const actionParams = (params.params as Record<string, unknown>) || {};
// 顶层参数验证
if (!action || typeof action !== 'string') {
logger.error('Missing or invalid action parameter', { params });
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: {
type: 'validation_error',
message: '必需提供 action 参数(字符串类型)'
}
})
}]
};
}
try {
const result = await executeAction(action, actionParams);
logger.action(action, actionParams, result);
return {
content: [{ type: 'text', text: JSON.stringify({ success: true, data: result }) }]
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Execution failed: ${action}`, { error: errorMessage });
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: {
type: 'execution_error',
message: errorMessage
}
})
}]
};
}
},
// 供测试使用的内部方法
getLimiterSummary(): string {
return limiter.getSummary();
},
resetLimiter(): void {
limiter.reset();
logger.info('Tool limiter reset');
}
};
}
export { ToolLimiter } from '../tool_limiter.js';
export function createGraphMemoryTool(sessionId?: string): GraphMemoryTool {
return new GraphMemoryTool(sessionId);
}

View File

@ -0,0 +1,20 @@
import type { Tool } from 'waterflow-ts/dist/runtime/core/tools/tool_interface.js';
import type { Platform } from 'waterflow-ts/dist/platform/types.js';
import type { ToolRegistry } from 'waterflow-ts/dist/runtime/core/tools/tool_registry.js';
import { GraphMemoryTool, createGraphMemoryTool, mapToolIdToApiName, mapApiNameToToolId } from './graph_memory_tool';
export { GraphMemoryTool, createGraphMemoryTool, mapToolIdToApiName, mapApiNameToToolId };
export function registerGraphMemoryTool(
registry: { register: (tool: Tool) => void },
sessionId?: string
): void {
registry.register(createGraphMemoryTool(sessionId));
}
export async function installTrulyMEM(platform: Platform, sessionId?: string): Promise<ToolRegistry> {
const { initializeToolRegistry } = await import('waterflow-ts/dist/runtime/core/tools/builtin/index.js');
const registry = initializeToolRegistry(platform);
registerGraphMemoryTool(registry, sessionId);
return registry;
}

View File

@ -1,139 +0,0 @@
export interface ToolLimits {
persona_query_max: number;
persona_update_max: number;
task_query_max: number;
task_update_max: number;
memory_query_max: number;
memory_update_max: number;
}
export interface ToolCallCount {
persona_query: number;
persona_update: number;
task_query: number;
task_update: number;
memory_query: number;
memory_update: number;
}
const DEFAULT_LIMITS: ToolLimits = {
persona_query_max: 1,
persona_update_max: 1,
task_query_max: 4,
task_update_max: 5,
memory_query_max: 20,
memory_update_max: 10,
};
export class ToolLimiter {
limits: ToolLimits;
counts: ToolCallCount;
constructor(limits?: Partial<ToolLimits>) {
this.limits = { ...DEFAULT_LIMITS, ...limits };
this.counts = {
persona_query: 0,
persona_update: 0,
task_query: 0,
task_update: 0,
memory_query: 0,
memory_update: 0,
};
}
private classifyTool(action: string): [string, string] {
switch (action) {
case 'persona_update':
case 'persona_clear':
return ['persona', 'update'];
case 'task_create':
case 'task_set_state':
case 'task_delete':
case 'task_link_info':
return ['task', 'update'];
case 'recall':
case 'introspect':
return ['memory', 'query'];
case 'commit':
case 'purge':
case 'archive':
case 'cleanup':
return ['memory', 'update'];
default:
return ['memory', 'update'];
}
}
canCall(action: string): [boolean, string] {
const [category, operation] = this.classifyTool(action);
if (category === 'persona') {
if (operation === 'query') {
if (this.counts.persona_query >= this.limits.persona_query_max) {
return [false, `人设图查询次数已达上限(${this.limits.persona_query_max}次)`];
}
} else {
if (this.counts.persona_update >= this.limits.persona_update_max) {
return [false, `人设图修改次数已达上限(${this.limits.persona_update_max}次)`];
}
}
} else if (category === 'task') {
if (operation === 'query') {
if (this.counts.task_query >= this.limits.task_query_max) {
return [false, `工作记忆链查询次数已达上限(${this.limits.task_query_max}次)`];
}
} else {
if (this.counts.task_update >= this.limits.task_update_max) {
return [false, `工作记忆链修改次数已达上限(${this.limits.task_update_max}次)`];
}
}
} else if (category === 'memory') {
if (operation === 'query') {
if (this.counts.memory_query >= this.limits.memory_query_max) {
return [false, `一般记忆查询次数已达上限(${this.limits.memory_query_max}次)`];
}
} else {
if (this.counts.memory_update >= this.limits.memory_update_max) {
return [false, `一般记忆修改次数已达上限(${this.limits.memory_update_max}次)`];
}
}
}
return [true, '允许调用'];
}
recordCall(action: string): void {
const [category, operation] = this.classifyTool(action);
if (category === 'persona') {
if (operation === 'query') this.counts.persona_query++;
else this.counts.persona_update++;
} else if (category === 'task') {
if (operation === 'query') this.counts.task_query++;
else this.counts.task_update++;
} else if (category === 'memory') {
if (operation === 'query') this.counts.memory_query++;
else this.counts.memory_update++;
}
}
getSummary(): string {
const lines = [
`人设图: 查询${this.counts.persona_query}/${this.limits.persona_query_max}次, 修改${this.counts.persona_update}/${this.limits.persona_update_max}`,
`工作记忆链: 查询${this.counts.task_query}/${this.limits.task_query_max}次, 修改${this.counts.task_update}/${this.limits.task_update_max}`,
`一般记忆: 查询${this.counts.memory_query}/${this.limits.memory_query_max}次, 修改${this.counts.memory_update}/${this.limits.memory_update_max}`,
];
return lines.join('\n');
}
reset(): void {
this.counts = {
persona_query: 0,
persona_update: 0,
task_query: 0,
task_update: 0,
memory_query: 0,
memory_update: 0,
};
}
}

29
ts/src/types/sql.js.d.ts vendored Normal file
View File

@ -0,0 +1,29 @@
declare module 'sql.js' {
export interface Database {
run(sql: string, params?: (string | number | null | Uint8Array)[]): void;
exec(sql: string): QueryExecResult[];
prepare(sql: string): Statement;
export(): Uint8Array;
close(): void;
}
export interface Statement {
bind(params?: (string | number | null | Uint8Array)[]): boolean;
step(): boolean;
getAsObject(): Record<string, unknown>;
free(): boolean;
}
export interface QueryExecResult {
columns: string[];
values: (string | number | null | Uint8Array)[][];
}
export interface SqlJsStatic {
Database: new (data?: ArrayLike<number>) => Database;
}
export default function initSqlJs(config?: {
locateFile?: (file: string) => string;
}): Promise<SqlJsStatic>;
}

637
ts/src/types/waterflow.d.ts vendored Normal file
View File

@ -0,0 +1,637 @@
declare module 'waterflow/platform' {
import type { Platform, CreatePlatformOptions, PlatformCapabilities } from 'waterflow-ts/platform/types';
export function getPlatform(): Platform;
export function hasCapability(capability: keyof PlatformCapabilities): boolean;
export function initPlatform(options?: CreatePlatformOptions): Platform;
export function resetPlatform(): void;
}
declare module 'waterflow/platform/types' {
export type BufferSource = ArrayBuffer | ArrayBufferView;
export type RuntimeType = 'node' | 'web' | 'harmony' | 'unknown';
export type OSType = 'windows' | 'macos' | 'linux' | 'android' | 'ios' | 'harmony' | 'unknown';
export interface PlatformCapabilities {
fileSystem: boolean;
processExecution: boolean;
network: boolean;
storage: boolean;
webSocket: boolean;
workers: boolean;
}
export interface PlatformAbortSignal {
readonly aborted: boolean;
readonly reason?: unknown;
addEventListener(type: 'abort', listener: () => void): void;
removeEventListener(type: 'abort', listener: () => void): void;
}
export interface PlatformAbortController {
readonly signal: PlatformAbortSignal;
abort(reason?: unknown): void;
}
export interface PlatformTextEncoder {
encode(input?: string): Uint8Array;
encodeInto(src: string, dest: Uint8Array): { read: number; written: number };
}
export interface PlatformTextDecoder {
decode(input?: BufferSource): string;
}
export interface PlatformURL {
href: string;
origin: string;
protocol: string;
host: string;
hostname: string;
port: string;
pathname: string;
search: string;
hash: string;
toString(): string;
toJSON(): string;
}
export interface PlatformGlobals {
TextEncoder: new (encoding?: string) => PlatformTextEncoder;
TextDecoder: new (encoding?: string) => PlatformTextDecoder;
URL: new (url: string) => { href: string; pathname: string; toString(): string };
randomUUID(): string;
now(): number;
btoa(data: string): string;
atob(data: string): string;
}
export interface GlobOptions {
cwd?: string;
ignore?: string[];
absolute?: boolean;
dot?: boolean;
onlyFiles?: boolean;
onlyDirectories?: boolean;
deep?: number;
ignoreCase?: boolean;
}
export interface GlobResult {
path: string;
isFile: boolean;
isDirectory: boolean;
}
export interface GlobTool {
glob(pattern: string, options?: GlobOptions): Promise<string[]>;
globWithInfo(pattern: string, options?: GlobOptions): Promise<GlobResult[]>;
isMatch(path: string, pattern: string): boolean;
search(pattern: string, basePath?: string): Promise<string[]>;
}
export interface GrepOptions {
cwd?: string;
ignoreCase?: boolean;
multiline?: boolean;
glob?: string | string[];
include?: string[];
exclude?: string[];
context?: number;
beforeContext?: number;
afterContext?: number;
headLimit?: number;
}
export interface GrepMatch {
path: string;
line: number;
column?: number;
content: string;
}
export interface GrepSearchOptions {
pattern: string;
path?: string;
glob?: string | string[];
ignoreCase?: boolean;
context?: number;
outputMode?: 'content' | 'files_with_matches' | 'count';
maxResults?: number;
}
export interface GrepSearchResult {
lines: string[];
}
export interface GrepTool {
grep(pattern: string | RegExp, options?: GrepOptions): Promise<GrepMatch[]>;
grepFiles(pattern: string | RegExp, options?: GrepOptions): Promise<string[]>;
grepCount(pattern: string | RegExp, options?: GrepOptions): Promise<number>;
search(options: GrepSearchOptions): Promise<GrepSearchResult>;
}
export interface PlatformTools {
glob: GlobTool | null;
grep: GrepTool | null;
}
export interface FileInfo {
path: string;
name: string;
isFile: boolean;
isDirectory: boolean;
size: number;
modifiedTime?: number;
createdTime?: number;
}
export interface FileReadOptions {
encoding?: 'utf-8' | 'binary' | 'base64';
start?: number;
end?: number;
}
export interface FileWriteOptions {
encoding?: 'utf-8' | 'binary' | 'base64';
append?: boolean;
createDir?: boolean;
}
export interface FileSystemOperations {
readFile(path: string, options?: FileReadOptions): Promise<string | ArrayBuffer>;
writeFile(path: string, data: string | ArrayBuffer, options?: FileWriteOptions): Promise<void>;
appendFile(path: string, data: string, options?: FileWriteOptions): Promise<void>;
deleteFile(path: string): Promise<void>;
exists(path: string): Promise<boolean>;
stat(path: string): Promise<FileInfo>;
readdir(path: string): Promise<FileInfo[]>;
mkdir(path: string, recursive?: boolean): Promise<void>;
rmdir(path: string, recursive?: boolean): Promise<void>;
copy(src: string, dest: string): Promise<void>;
move(src: string, dest: string): Promise<void>;
watch?(path: string, callback: (event: string, filename: string) => void): () => void;
}
export interface ProcessResult {
exitCode: number;
stdout: string;
stderr: string;
signal?: string;
}
export interface ProcessOptions {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
input?: string;
shell?: boolean;
maxBuffer?: number;
}
export interface ProcessOperations {
exec(command: string, options?: ProcessOptions): Promise<ProcessResult>;
execFile(file: string, args: string[], options?: ProcessOptions): Promise<ProcessResult>;
spawn?(command: string, args: string[], options?: ProcessOptions): AsyncIterable<string>;
which?(command: string): Promise<string | null>;
kill?(pid: number, signal?: string): Promise<boolean>;
}
export interface StorageOperations {
get(key: string): Promise<string | null>;
set(key: string, value: string, ttl?: number): Promise<void>;
delete(key: string): Promise<void>;
clear(): Promise<void>;
keys(): Promise<string[]>;
}
export interface PathOperations {
join(...paths: string[]): string;
dirname(path: string): string;
basename(path: string, ext?: string): string;
extname(path: string): string;
normalize(path: string): string;
isAbsolute(path: string): boolean;
resolve(...paths: string[]): string;
relative(from: string, to: string): string;
}
export interface PlatformResponse {
status: number;
statusText: string;
headers: Record<string, string>;
ok: boolean;
text(): Promise<string>;
json(): Promise<any>;
arrayBuffer(): Promise<ArrayBuffer>;
}
export interface PlatformFetchRequestInit {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
headers?: Record<string, string>;
body?: string | ArrayBuffer | Record<string, string | ArrayBuffer>;
timeout?: number;
}
export interface PlatformWebSocket {
readonly readyState: number;
readonly url: string;
send(data: string | ArrayBuffer): void;
close(code?: number, reason?: string): void;
addEventListener(type: string, listener: (event: any) => void): void;
removeEventListener(type: string, listener: (event: any) => void): void;
}
export interface NetworkOperations {
fetch(url: string, options?: PlatformFetchRequestInit): Promise<PlatformResponse>;
fetchStream?(url: string, options?: PlatformFetchRequestInit): AsyncGenerator<ArrayBuffer, PlatformResponse, unknown>;
connectWebSocket?(url: string, protocols?: string[]): Promise<PlatformWebSocket>;
}
export interface PlatformInfo {
runtime: RuntimeType;
os: OSType;
version?: string;
arch?: string;
hostname?: string;
capabilities: PlatformCapabilities;
}
export interface Tool {
readonly id: string;
readonly name: string;
readonly apiName?: string;
readonly description: string;
readonly category: ToolCategory;
readonly inputSchema: ToolInputSchema;
readonly outputSchema?: ToolOutputSchema;
readonly handler: (params: ToolInput, context: ToolExecutionContext) => Promise<ToolOutput>;
readonly permissionLevel: ToolPermissionLevel;
readonly requiredPermissions?: string[];
readonly features?: ToolFeatures;
readonly metadata?: ToolMetadata;
readonly isMcp?: boolean;
readonly shouldDefer?: boolean;
readonly alwaysLoad?: boolean;
readonly searchHint?: string;
prompt?(options: ToolPromptOptions): Promise<string>;
}
export interface Platform {
getInfo(): PlatformInfo;
createAbortController(): PlatformAbortController;
readonly path: PathOperations;
readonly fs: FileSystemOperations | null;
readonly process: ProcessOperations | null;
readonly storage: StorageOperations;
readonly network: NetworkOperations;
readonly globals: PlatformGlobals;
readonly tools: PlatformTools;
readonly builtinTools: Tool[];
getEnv(key: string): string | undefined;
getAllEnv?(): Record<string, string>;
setEnv?(key: string, value: string): void;
getCwd(): string;
setCwd?(path: string): void;
exit?(code: number): void;
}
export interface CreatePlatformOptions {
storagePath?: string;
storageType?: 'localStorage' | 'indexedDB';
dbName?: string;
context?: any;
storageName?: string;
}
export type ToolInput = Record<string, unknown>;
export type ToolOutput = string | Record<string, unknown> | void;
export type ToolPermissionLevel = 'safe' | 'moderate' | 'dangerous' | 'restricted';
export type ToolCategory = 'file' | 'code' | 'search' | 'execute' | 'network' | 'analysis' | 'generation' | 'communication' | 'custom';
export interface ToolSchemaProperty {
type: 'string' | 'number' | 'integer' | 'boolean' | 'array' | 'object';
description: string;
enum?: string[];
default?: unknown;
examples?: unknown[];
items?: ToolSchemaProperty;
properties?: Record<string, ToolSchemaProperty>;
}
export interface ToolInputSchema {
type: 'object';
properties: Record<string, ToolSchemaProperty>;
required?: string[];
additionalProperties?: boolean;
}
export interface ToolExecutionContext {
toolCallId: string;
workingDirectory: string;
additionalWorkingDirectories?: string[];
abortController: PlatformAbortController;
config: {
timeout?: number;
maxOutputSize?: number;
allowedDirectories?: string[];
};
logger: {
info(message: string, ...args: unknown[]): void;
warn(message: string, ...args: unknown[]): void;
error(message: string, ...args: unknown[]): void;
debug(message: string, ...args: unknown[]): void;
};
}
export interface BuiltinTool {
readonly id: string;
readonly name: string;
readonly apiName?: string;
readonly description: string;
readonly category: ToolCategory;
readonly inputSchema: ToolInputSchema;
readonly permissionLevel: ToolPermissionLevel;
handler(params: ToolInput, context: ToolExecutionContext): Promise<ToolOutput>;
}
}
declare module 'waterflow/runtime/core/tools/tool_interface' {
import type { PlatformAbortController } from 'waterflow-ts/platform/types';
import type { AgentId } from 'waterflow-ts/shared/types/agent';
import type { WorkflowRunner } from 'waterflow-ts/runtime/core/workflow/types';
import type { WorkflowRegistryImpl } from 'waterflow-ts/runtime/core/workflow/workflow_registry';
import type { AgentRegistryImpl } from 'waterflow-ts/runtime/core/workflow/agent_registry';
import type { AgentExecutor } from 'waterflow-ts/runtime/core/agent/agent_executor';
import type { MCPClient } from 'waterflow-ts/runtime/core/tools/mcp/mcp_client';
export type ToolCategory =
| 'file'
| 'code'
| 'search'
| 'execute'
| 'network'
| 'analysis'
| 'generation'
| 'communication'
| 'mcp'
| 'custom';
export type PermissionLevel =
| 'safe'
| 'moderate'
| 'dangerous'
| 'restricted';
export type SchemaType =
| 'string'
| 'number'
| 'integer'
| 'boolean'
| 'array'
| 'object';
export interface SchemaProperty {
type: SchemaType;
description: string;
enum?: string[];
minimum?: number;
maximum?: number;
minLength?: number;
maxLength?: number;
pattern?: string;
default?: any;
examples?: any[];
suggestedSource?: 'context' | 'literal' | 'file';
items?: SchemaProperty;
properties?: Record<string, SchemaProperty>;
required?: string[];
additionalProperties?: boolean | SchemaProperty;
}
export interface ToolInputSchema {
type: 'object';
properties: Record<string, SchemaProperty>;
required?: string[];
additionalProperties?: boolean;
semanticHints?: Record<string, string>;
}
export interface ToolOutputSchema {
type: 'object';
properties: Record<string, SchemaProperty>;
format?: 'json' | 'text' | 'markdown' | 'binary';
maxSize?: number;
maxLines?: number;
}
export type ToolInput = Record<string, unknown>;
export type ToolOutput = string | Record<string, unknown> | void;
export interface Logger {
info(message: string, ...args: any[]): void;
warn(message: string, ...args: any[]): void;
error(message: string, ...args: any[]): void;
debug(message: string, ...args: any[]): void;
}
export interface ToolConfig {
timeout?: number;
maxOutputSize?: number;
allowedDirectories?: string[];
}
export interface ToolExecutionContext {
toolCallId: string;
agentId?: AgentId;
workingDirectory: string;
additionalWorkingDirectories?: string[] | undefined;
abortController: PlatformAbortController;
config: ToolConfig;
logger: Logger;
workflowRunner?: WorkflowRunner | undefined;
workflowRegistry?: WorkflowRegistryImpl | undefined;
agentExecutor?: AgentExecutor | undefined;
agentRegistry?: AgentRegistryImpl | undefined;
allowedAgentTypes?: string[] | undefined;
mcpClients?: Map<string, MCPClient> | undefined;
tools?: Tool[] | undefined;
}
export type ToolHandler = (
params: ToolInput,
context: ToolExecutionContext
) => Promise<ToolOutput>;
export interface ToolFeatures {
isAsync?: boolean;
isStreamable?: boolean;
isCacheable?: boolean;
requiresConfirmation?: boolean;
supportsProgress?: boolean;
supportsCancellation?: boolean;
producesFiles?: boolean;
producesImages?: boolean;
producesStructuredOutput?: boolean;
requiresMcp?: boolean;
requiresNetwork?: boolean;
}
export interface ToolExample {
description: string;
input: ToolInput;
output: ToolOutput;
explanation?: string;
}
export interface ToolMetadata {
source: 'builtin' | 'mcp' | 'plugin' | 'external';
version?: string;
author?: string;
documentationUrl?: string;
examples?: ToolExample[];
estimatedDuration?: number;
estimatedTokens?: number;
compatibleModels?: string[];
incompatibleModels?: string[];
}
export interface ToolPromptOptions {
tools: Tool[];
agentRegistry?: AgentRegistryImpl | undefined;
workflowRegistry?: WorkflowRegistryImpl | undefined;
allowedAgentTypes?: string[] | undefined;
}
export interface Tool {
readonly id: string;
readonly name: string;
readonly apiName?: string;
readonly description: string;
readonly category: ToolCategory;
readonly inputSchema: ToolInputSchema;
readonly outputSchema?: ToolOutputSchema;
readonly handler: ToolHandler;
readonly permissionLevel: PermissionLevel;
readonly requiredPermissions?: string[];
readonly features?: ToolFeatures;
readonly metadata?: ToolMetadata;
readonly isMcp?: boolean;
readonly shouldDefer?: boolean;
readonly alwaysLoad?: boolean;
readonly searchHint?: string;
prompt?(options: ToolPromptOptions): Promise<string>;
}
export interface ToolExecutionError {
type: ToolErrorType;
message: string;
code?: string;
details?: Record<string, unknown>;
}
export type ToolErrorType =
| 'validation_error'
| 'permission_denied'
| 'timeout'
| 'execution_error'
| 'network_error'
| 'mcp_error'
| 'unknown_error';
export interface ToolExecutionResult {
success: boolean;
toolId: string;
toolCallId: string;
output: ToolOutput;
error?: ToolExecutionError;
metadata: {
duration: number;
tokensUsed?: number;
cached?: boolean;
retryCount?: number;
};
}
export interface ToolCall {
id: string;
toolId: string;
toolName: string;
input: ToolInput;
callerId: AgentId | string;
callerType: 'agent' | 'workflow' | 'main';
}
export function createTextOutput(text: string): ToolOutput;
export function createJSONOutput(data: Record<string, unknown>): ToolOutput;
export function createErrorOutput(message: string, code?: string, details?: Record<string, unknown>): ToolOutput;
}
declare module 'waterflow/runtime/core/tools/builtin' {
import type { Platform } from 'waterflow-ts/platform/types';
import type { Tool } from 'waterflow-ts/runtime/core/tools/tool_interface';
import { ToolRegistry } from 'waterflow-ts/runtime/core/tools/tool_registry';
export const FRAMEWORK_TOOLS: Tool[];
export function initializeToolRegistry(platform: Platform): ToolRegistry;
export function getFrameworkTools(): Tool[];
}
declare module 'waterflow/runtime/core/tools/tool_registry' {
import type { Tool, ToolCategory, ToolInput } from 'waterflow-ts/runtime/core/tools/tool_interface';
export interface ValidationResult {
valid: boolean;
errors?: string[];
}
export interface ToolSearchResult {
tool: Tool;
relevanceScore: number;
matchReason: string;
}
export interface SearchOptions {
category?: ToolCategory;
permissionLevel?: string;
limit?: number;
}
export interface RegistryStatistics {
totalTools: number;
byCategory: Record<string, number>;
bySource: Record<string, number>;
byPermissionLevel: Record<string, number>;
}
export interface ToolDefinitionExtended {
id: string;
name: string;
description: string;
parameters: {
type: 'object';
properties: Record<string, any>;
required?: string[];
additionalProperties?: boolean;
};
execute: (params: Record<string, any>) => Promise<any>;
}
export class ToolRegistry {
register(tool: Tool | ToolDefinitionExtended): void;
registerAll(tools: (Tool | ToolDefinitionExtended)[]): void;
unregister(toolId: string): void;
get(toolId: string): Tool | ToolDefinitionExtended | undefined;
getByName(name: string): Tool | ToolDefinitionExtended | undefined;
has(toolId: string): boolean;
size(): number;
listAll(): (Tool | ToolDefinitionExtended)[];
listByCategory(category: ToolCategory): (Tool | ToolDefinitionExtended)[];
listByPermissionLevel(level: string): (Tool | ToolDefinitionExtended)[];
search(query: string, options?: SearchOptions): ToolSearchResult[];
isAvailable(toolId: string, context?: any): boolean;
validateInput(toolId: string, input: ToolInput): ValidationResult;
clear(): void;
getStatistics(): RegistryStatistics;
}
}

View File

@ -1,222 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GraphDatabase } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/graph_database.js';
const TEST_DB_PATH = '/tmp/test_graph_memory.db';
describe('GraphDatabase', () => {
let db: GraphDatabase;
beforeEach(async () => {
try {
const fs = await import('fs');
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
if (fs.existsSync(`${TEST_DB_PATH}-wal`)) {
fs.unlinkSync(`${TEST_DB_PATH}-wal`);
}
if (fs.existsSync(`${TEST_DB_PATH}-shm`)) {
fs.unlinkSync(`${TEST_DB_PATH}-shm`);
}
} catch {}
db = new GraphDatabase(TEST_DB_PATH);
});
afterEach(() => {
if (db && typeof db.close === 'function') {
db.close();
}
});
describe('commit', () => {
it('creates entities and relations', async () => {
const result = await db.commit({
triplets: [
{ subject: 'Alice', relation: 'knows', object: 'Bob' }
]
});
expect(result.createdEntities).toBe(2);
expect(result.createdRelations).toBe(1);
});
it('handles batch multiple triplets', async () => {
const result = await db.commit({
triplets: [
{ subject: '用户', relation: '喜欢', object: '编程' },
{ subject: '用户', relation: '正在学习', object: 'TypeScript' },
{ subject: 'TypeScript', relation: '是', object: '语言' }
]
});
expect(result.createdEntities).toBe(6);
expect(result.createdRelations).toBe(3);
});
});
describe('recall', () => {
beforeEach(async () => {
await db.commit({
triplets: [
{ subject: '用户', relation: '喜欢', object: '编程' },
{ subject: '用户', relation: '正在学习', object: 'TypeScript' },
{ subject: 'TypeScript', relation: '是', object: '语言' },
{ subject: 'Alice', relation: 'knows', object: 'Bob' }
]
});
});
it('keyword search finds matching entities', async () => {
const result = await db.recall({ queryIntent: '用户' });
expect(result.entities.length).toBeGreaterThan(0);
expect(result.entities.some(e => e.name === '用户')).toBe(true);
});
it('seedEntities finds exact matches', async () => {
const result = await db.recall({
queryIntent: 'test',
seedEntities: ['用户']
});
expect(result.entities.length).toBeGreaterThan(0);
expect(result.entities.some(e => e.name === '用户')).toBe(true);
});
it('sessionFilter filters by session', async () => {
await db.commit({
triplets: [
{ subject: 'Test', relation: 'is', object: 'Temp' }
],
sessionId: 'test-session'
});
const result = await db.recall({
queryIntent: 'Test',
sessionFilter: 'test-session'
});
expect(result.entities.some(e => e.name === 'Test')).toBe(true);
});
it('finds related entities through relations', async () => {
const result = await db.recall({
queryIntent: '编程'
});
expect(result.relations.length).toBeGreaterThan(0);
});
});
describe('purge', () => {
beforeEach(async () => {
await db.commit({
triplets: [
{ subject: 'ToDelete', relation: 'is', object: 'Test' }
]
});
});
it('soft delete marks relations as deleted', async () => {
const result = await db.purge({
criteria: { subject: 'ToDelete' },
mode: 'soft'
});
expect(result.deleted).toBe(1);
expect(result.mode).toBe('soft');
});
it('hard delete removes relations permanently', async () => {
const result = await db.purge({
criteria: { subject: 'ToDelete' },
mode: 'hard'
});
expect(result.deleted).toBe(1);
expect(result.mode).toBe('hard');
});
it('purge filters by target criteria', async () => {
const result = await db.purge({
criteria: { target: 'Test' },
mode: 'soft'
});
expect(result.deleted).toBe(1);
});
it('purge filters by relation criteria', async () => {
const result = await db.purge({
criteria: { relation: 'is' },
mode: 'soft'
});
expect(result.deleted).toBe(1);
});
});
describe('introspect', () => {
it('returns entity and relation counts', async () => {
await db.commit({
triplets: [
{ subject: 'Entity1', relation: 'relates', object: 'Entity2' }
]
});
const stats = await db.introspect();
expect(stats.entityCount).toBe(2);
expect(stats.relationCount).toBe(1);
expect(stats.sessionId).toBeDefined();
});
});
describe('empty query handling', () => {
it('returns empty result for query with no matches', async () => {
const result = await db.recall({ queryIntent: 'xyznonexistent123' });
expect(result.entities).toEqual([]);
expect(result.relations).toEqual([]);
});
});
describe('duplicate entity handling', () => {
it('upserts existing entities (increments mention count)', async () => {
await db.commit({
triplets: [
{ subject: 'Duplicate', relation: 'is', object: 'First' }
]
});
const stats1 = await db.introspect();
const entityBefore = stats1.entityCount;
await db.commit({
triplets: [
{ subject: 'Duplicate', relation: 'is', object: 'Second' }
]
});
const stats2 = await db.introspect();
const entityAfter = stats2.entityCount;
expect(entityAfter).toBeLessThan(entityBefore + 2);
});
});
describe('setSessionId and getSessionId', () => {
it('setSessionId updates session id', async () => {
db.setSessionId('new-session-id');
expect(db.getSessionId()).toBe('new-session-id');
});
it('getSessionId returns current session id', async () => {
const sessionId = db.getSessionId();
expect(sessionId).toBeDefined();
expect(typeof sessionId).toBe('string');
});
});
});

View File

@ -1,242 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MemoryService } from '../../../../dist/runtime/core/graph_memory/memory_service.js';
import { GraphDatabase } from '../../../../dist/runtime/core/graph_memory/graph_database.js';
import * as fs from 'fs';
const TEST_DB_PATH = '/tmp/test_memory_service.db';
describe('MemoryService', () => {
let memoryService: MemoryService;
let db: GraphDatabase;
beforeEach(async () => {
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
const walPath = TEST_DB_PATH + '-wal';
const shmPath = TEST_DB_PATH + '-shm';
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
db = new GraphDatabase(TEST_DB_PATH, 'test-session');
memoryService = new MemoryService(db);
});
afterEach(async () => {
if (typeof db.close === 'function') db.close();
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
const walPath = TEST_DB_PATH + '-wal';
const shmPath = TEST_DB_PATH + '-shm';
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
});
describe('updatePersona', () => {
it('should merge attributes in merge mode', async () => {
const result = await memoryService.updatePersona({
attributes: [
{ attribute: 'name', value: 'AI Assistant' },
{ attribute: 'personality', value: 'helpful' }
],
mode: 'merge'
});
expect(result.status).toBe('success');
expect(result.updatedAttributes).toBe(2);
});
it('should replace attributes in replace mode', async () => {
await memoryService.updatePersona({
attributes: [
{ attribute: 'name', value: 'Old Name' }
],
mode: 'merge'
});
const result = await memoryService.updatePersona({
attributes: [
{ attribute: 'name', value: 'New Name' }
],
mode: 'replace'
});
expect(result.status).toBe('success');
expect(result.updatedAttributes).toBe(1);
});
});
describe('clearPersona', () => {
it('should delete persona when confirm is true', async () => {
await memoryService.updatePersona({
attributes: [
{ attribute: 'name', value: 'Test AI' }
],
mode: 'merge'
});
const result = await memoryService.clearPersona({ confirm: true });
expect(result.status).toBe('success');
expect(result.deletedCount).toBeGreaterThan(0);
});
it('should cancel when confirm is false', async () => {
const result = await memoryService.clearPersona({ confirm: false });
expect(result.status).toBe('cancelled');
expect(result.deletedCount).toBe(0);
});
});
describe('createTask', () => {
it('should create task with info_nodes', async () => {
const result = await memoryService.createTask({
task_id: 'Task_001',
description: 'Test task with info',
info_nodes: ['Node_A', 'Node_B']
});
expect(result.status).toBe('success');
expect(result.taskId).toBe('Task_001');
});
it('should create task without info_nodes', async () => {
const result = await memoryService.createTask({
task_id: 'Task_002',
description: 'Test task without info'
});
expect(result.status).toBe('success');
expect(result.taskId).toBe('Task_002');
});
});
describe('setTaskState', () => {
it('should set task state', async () => {
await memoryService.createTask({
task_id: 'Task_StateTest',
description: 'Task for state test'
});
const result = await memoryService.setTaskState({
task_id: 'Task_StateTest',
state: '已暂停'
});
expect(result.status).toBe('success');
expect(result.newState).toBe('已暂停');
});
});
describe('deleteTask', () => {
it('should delete task', async () => {
await memoryService.createTask({
task_id: 'Task_Delete',
description: 'Task to delete'
});
const result = await memoryService.deleteTask({ task_id: 'Task_Delete' });
expect(result.status).toBe('success');
expect(result.taskId).toBe('Task_Delete');
});
});
describe('linkInfoToTask', () => {
it('should link info node to task', async () => {
await memoryService.createTask({
task_id: 'Task_Link',
description: 'Task for linking'
});
const result = await memoryService.linkInfoToTask({
task_id: 'Task_Link',
info_node: 'Info_Node_X'
});
expect(result.status).toBe('success');
});
});
describe('sessionId', () => {
it('should set and get sessionId', () => {
memoryService.setSessionId('custom-session-123');
const sessionId = memoryService.getSessionId();
expect(sessionId).toBe('custom-session-123');
});
});
describe('task state transitions', () => {
it('should transition: 进行中 → 已暂停 → 进行中 → 已完成', async () => {
await memoryService.createTask({
task_id: 'Task_Transition',
description: 'State transition task'
});
const state1 = await memoryService.setTaskState({
task_id: 'Task_Transition',
state: '进行中'
});
expect(state1.newState).toBe('进行中');
const state2 = await memoryService.setTaskState({
task_id: 'Task_Transition',
state: '已暂停'
});
expect(state2.newState).toBe('已暂停');
const state3 = await memoryService.setTaskState({
task_id: 'Task_Transition',
state: '进行中'
});
expect(state3.newState).toBe('进行中');
const state4 = await memoryService.setTaskState({
task_id: 'Task_Transition',
state: '已完成'
});
expect(state4.newState).toBe('已完成');
});
});
describe('delegate methods', () => {
it('recall should delegate to db', async () => {
await memoryService.commit({
triplets: [
{ subject: '测试', relation: '是', object: '示例' }
]
});
const result = await memoryService.recall({
queryIntent: '测试'
});
expect(result.entities).toBeDefined();
expect(result.relations).toBeDefined();
});
it('commit should delegate to db', async () => {
const result = await memoryService.commit({
triplets: [
{ subject: '用户', relation: '喜欢', object: '编程' }
]
});
expect(result.createdEntities).toBeGreaterThan(0);
expect(result.createdRelations).toBeGreaterThan(0);
});
it('purge should delegate to db', async () => {
await memoryService.commit({
triplets: [
{ subject: '待删除', relation: '是', object: '测试' }
]
});
const result = await memoryService.purge({
criteria: { subject: '待删除' }
});
expect(result.deleted).toBeGreaterThanOrEqual(0);
});
});
});

View File

@ -1,200 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { GraphDatabase } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/graph_database.js';
import { MemoryService } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/memory_service.js';
import { TaskNodeStore } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/task_node_store.js';
import * as fs from 'fs';
import * as path from 'path';
const TEST_DB_PATH = '/tmp/test_memory_service_p2.db';
const TEST_ARCHIVE_DIR = '/tmp/test_context_archive';
describe('MemoryService P2 Advanced Features', () => {
let memoryService: MemoryService;
let db: GraphDatabase;
let taskStore: TaskNodeStore;
beforeEach(async () => {
// Clean up test files
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
const walPath = TEST_DB_PATH + '-wal';
const shmPath = TEST_DB_PATH + '-shm';
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
db = new GraphDatabase(TEST_DB_PATH, 'test-session-p2');
taskStore = new TaskNodeStore(TEST_DB_PATH, TEST_ARCHIVE_DIR + '/task');
memoryService = new MemoryService(db, taskStore, TEST_ARCHIVE_DIR);
});
afterEach(() => {
if (typeof db.close === 'function') db.close();
if (typeof taskStore.close === 'function') taskStore.close();
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
});
describe('context_rewrite', () => {
it('should compress context and extract key entities', async () => {
const longContext = `用户说我喜欢用Python编程。最近在学习TypeScript因为想做一个全栈项目。
我决定采用React作为前端框架后端用FastAPI。数据库选择PostgreSQL。
这个决策对我来说很重要,因为我希望能快速迭代。我对性能有较高要求。
目标是三个月内上线第一个版本。`;
const result = await memoryService.contextRewrite({
context: longContext,
maxEntities: 10
});
expect(result.extractedEntities).toBeGreaterThan(0);
expect(result.summary.length).toBeGreaterThan(0);
expect(result.compressed).toBe(true);
});
it('should detect preferences and decisions', async () => {
const context = `用户决定使用React而不是Vue。用户喜欢简洁的代码风格。
用户习惯每天早上检查代码质量。用户选择PostgreSQL作为数据库。`;
const result = await memoryService.contextRewrite({
context,
summary: '用户技术偏好总结'
});
expect(result.extractedRelations).toBeGreaterThan(0);
expect(result.summary).toBe('用户技术偏好总结');
});
});
describe('working_memory_chain', () => {
it('should retrieve working memory from task nodes', async () => {
// First create some task nodes
await memoryService.createTaskNode({
session_id: 'test-session-p2',
turn_id: 1,
summary: '用户询问天气',
key_facts: ['意图: 查询天气', '地点: 北京']
});
await memoryService.createTaskNode({
session_id: 'test-session-p2',
turn_id: 2,
summary: '用户询问交通',
key_facts: ['意图: 查询交通', '地点: 北京']
});
const result = await memoryService.workingMemoryChain({
maxDepth: 2,
recentOnly: true
});
expect(result.chain.length).toBeGreaterThan(0);
expect(result.entityCount).toBeGreaterThan(0);
});
});
describe('task_node chain', () => {
it('should create task nodes and link them in chain', async () => {
const node1 = await memoryService.createTaskNode({
session_id: 'chain-test',
turn_id: 1,
summary: '开始对话',
key_facts: ['fact1', 'fact2'],
raw_context: '用户: 你好\nAI: 你好!有什么可以帮你的?'
});
expect(node1.node_id).toBeDefined();
expect(node1.chain_linked).toBe(false); // First node
const node2 = await memoryService.createTaskNode({
session_id: 'chain-test',
turn_id: 2,
summary: '用户询问编程',
key_facts: ['fact3'],
raw_context: '用户: 我想学编程\nAI: 太好了!你想学什么语言?'
});
expect(node2.node_id).toBeDefined();
expect(node2.chain_linked).toBe(true); // Linked to first node
});
it('should get recent task nodes', async () => {
await memoryService.createTaskNode({
session_id: 'recent-test',
turn_id: 1,
summary: 'Node 1',
key_facts: ['fact1']
});
await memoryService.createTaskNode({
session_id: 'recent-test',
turn_id: 2,
summary: 'Node 2',
key_facts: ['fact2']
});
await memoryService.createTaskNode({
session_id: 'recent-test',
turn_id: 3,
summary: 'Node 3',
key_facts: ['fact3']
});
const recent = await memoryService.getRecentTaskNodes('recent-test', 2);
expect(recent.length).toBe(2);
expect(recent[0].turn_id).toBe(2); // Chronological order
expect(recent[1].turn_id).toBe(3);
});
it('should get full task chain', async () => {
await memoryService.createTaskNode({
session_id: 'chain-full-test',
turn_id: 1,
summary: 'Start',
key_facts: ['start']
});
await memoryService.createTaskNode({
session_id: 'chain-full-test',
turn_id: 2,
summary: 'Middle',
key_facts: ['middle']
});
await memoryService.createTaskNode({
session_id: 'chain-full-test',
turn_id: 3,
summary: 'End',
key_facts: ['end']
});
const chain = await memoryService.getTaskChain('chain-full-test');
expect(chain.nodes.length).toBe(3);
expect(chain.relations.length).toBe(2); // 3 nodes = 2 next relations
expect(chain.relations[0].type).toBe('next');
});
it('should archive and read raw context', async () => {
const rawContext = '这是一个很长的对话记录...包含很多细节...';
await memoryService.createTaskNode({
session_id: 'archive-test',
turn_id: 1,
summary: '对话摘要',
key_facts: ['fact1'],
raw_context: rawContext
});
const readBack = await memoryService.readArchivedContext('archive-test', 1);
expect(readBack).toBe(rawContext);
});
});
});

View File

@ -1,155 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TaskNodeStore } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/task_node_store.js';
import * as fs from 'fs';
const TEST_DB_PATH = '/tmp/test_task_node_store.db';
const TEST_ARCHIVE_DIR = '/tmp/test_task_archive';
describe('TaskNodeStore', () => {
let store: TaskNodeStore;
beforeEach(async () => {
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
const walPath = TEST_DB_PATH + '-wal';
const shmPath = TEST_DB_PATH + '-shm';
if (fs.existsSync(walPath)) fs.unlinkSync(walPath);
if (fs.existsSync(shmPath)) fs.unlinkSync(shmPath);
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
store = new TaskNodeStore(TEST_DB_PATH, TEST_ARCHIVE_DIR);
});
afterEach(() => {
if (typeof store.close === 'function') store.close();
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
if (fs.existsSync(TEST_ARCHIVE_DIR)) {
fs.rmSync(TEST_ARCHIVE_DIR, { recursive: true, force: true });
}
});
describe('createTaskNode', () => {
it('should create a task node', async () => {
const result = await store.createTaskNode({
session_id: 'session-1',
turn_id: 1,
summary: 'Test summary',
key_facts: ['fact1', 'fact2']
});
expect(result.node_id).toBeDefined();
expect(result.chain_linked).toBe(false);
});
it('should link nodes in chain', async () => {
await store.createTaskNode({
session_id: 'chain-session',
turn_id: 1,
summary: 'First',
key_facts: ['fact1']
});
const result = await store.createTaskNode({
session_id: 'chain-session',
turn_id: 2,
summary: 'Second',
key_facts: ['fact2']
});
expect(result.chain_linked).toBe(true);
});
it('should archive raw context', async () => {
const rawContext = 'This is a long context...';
await store.createTaskNode({
session_id: 'archive-session',
turn_id: 1,
summary: 'Archived',
key_facts: ['fact1'],
raw_context: rawContext
});
const readBack = await store.readArchivedContext('archive-session', 1);
expect(readBack).toBe(rawContext);
});
});
describe('getRecentTaskNodes', () => {
it('should return recent nodes in chronological order', async () => {
await store.createTaskNode({
session_id: 'recent-session',
turn_id: 1,
summary: 'One',
key_facts: ['fact1']
});
await store.createTaskNode({
session_id: 'recent-session',
turn_id: 2,
summary: 'Two',
key_facts: ['fact2']
});
await store.createTaskNode({
session_id: 'recent-session',
turn_id: 3,
summary: 'Three',
key_facts: ['fact3']
});
const recent = await store.getRecentTaskNodes('recent-session', 2);
expect(recent.length).toBe(2);
expect(recent[0].turn_id).toBe(2);
expect(recent[1].turn_id).toBe(3);
});
});
describe('getTaskChain', () => {
it('should walk full chain backwards', async () => {
const n1 = await store.createTaskNode({
session_id: 'walk-session',
turn_id: 1,
summary: 'Start',
key_facts: ['start']
});
const n2 = await store.createTaskNode({
session_id: 'walk-session',
turn_id: 2,
summary: 'Middle',
key_facts: ['middle']
});
await store.createTaskNode({
session_id: 'walk-session',
turn_id: 3,
summary: 'End',
key_facts: ['end']
});
const chain = await store.getTaskChain('walk-session');
expect(chain.nodes.length).toBe(3);
expect(chain.relations.length).toBe(2);
});
it('should walk chain from specific node', async () => {
await store.createTaskNode({
session_id: 'from-session',
turn_id: 1,
summary: 'A',
key_facts: ['a']
});
const n2 = await store.createTaskNode({
session_id: 'from-session',
turn_id: 2,
summary: 'B',
key_facts: ['b']
});
const chain = await store.getTaskChain('from-session', n2.node_id);
expect(chain.nodes.length).toBe(2);
});
});
});

View File

@ -1,692 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createGraphMemoryTool, ToolLimiter } from '../../../../../dist/runtime/core/tools/builtin/graph_memory_tool.js';
import * as fs from 'fs';
const TEST_DB_PATH = '/tmp/test_graph_memory_tool.db';
describe('GraphMemoryTool', () => {
let tool: ReturnType<typeof createGraphMemoryTool>;
beforeEach(() => {
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
tool = createGraphMemoryTool(TEST_DB_PATH, 'test-session');
});
afterEach(() => {
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
});
describe('Tool metadata', () => {
it('should have correct name', () => {
expect(tool.name).toBe('graph_memory');
});
it('should have description', () => {
expect(tool.description).toBeTruthy();
expect(tool.description).toContain('图记忆');
});
it('should have input schema with all actions', () => {
const actions = tool.parameters.properties?.action?.enum as string[];
expect(actions).toContain('recall');
expect(actions).toContain('commit');
expect(actions).toContain('purge');
expect(actions).toContain('introspect');
expect(actions).toContain('persona_update');
expect(actions).toContain('persona_clear');
expect(actions).toContain('task_create');
expect(actions).toContain('task_set_state');
expect(actions).toContain('task_delete');
expect(actions).toContain('task_link_info');
expect(actions).toContain('archive');
expect(actions).toContain('cleanup');
});
});
describe('execute wrapper', () => {
it('should execute recall action', async () => {
await tool.execute('call-1', {
action: 'commit',
params: {
triplets: [
{ subject: '用户', relation: '喜欢', object: '编程' }
]
}
});
const result = await tool.execute('call-2', {
action: 'recall',
params: {
queryIntent: '用户 编程'
}
});
expect(result).toHaveProperty('content');
expect(Array.isArray(result.content)).toBe(true);
expect(result.content[0]).toHaveProperty('type', 'text');
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.entities).toBeDefined();
expect(parsed.data.relations).toBeDefined();
});
it('should return empty results for non-matching query', async () => {
const result = await tool.execute('call-3', {
action: 'recall',
params: {
queryIntent: '不存在的关键词xyz123'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.entities).toEqual([]);
expect(parsed.data.relations).toEqual([]);
});
it('should commit triplets successfully', async () => {
const result = await tool.execute('call-4', {
action: 'commit',
params: {
triplets: [
{ subject: '测试', relation: '是', object: '示例' }
]
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.createdEntities).toBeGreaterThan(0);
expect(parsed.data.createdRelations).toBeGreaterThan(0);
});
it('should handle multiple triplets', async () => {
const result = await tool.execute('call-5', {
action: 'commit',
params: {
triplets: [
{ subject: '实体A', relation: '关联', object: '实体B' },
{ subject: '实体B', relation: '关联', object: '实体C' },
{ subject: '实体C', relation: '关联', object: '实体A' }
]
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.createdEntities).toBe(6);
expect(parsed.data.createdRelations).toBe(3);
});
it('should purge relations by criteria', async () => {
await tool.execute('call-6', {
action: 'commit',
params: {
triplets: [
{ subject: '待删除', relation: '是', object: '测试' }
]
}
});
const result = await tool.execute('call-7', {
action: 'purge',
params: {
criteria: { subject: '待删除' },
mode: 'soft'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.deleted).toBeGreaterThanOrEqual(1);
expect(parsed.data.mode).toBe('soft');
});
it('should return 0 when no criteria provided', async () => {
const result = await tool.execute('call-8', {
action: 'purge',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.deleted).toBe(0);
});
it('should return memory statistics', async () => {
await tool.execute('call-9', {
action: 'commit',
params: {
triplets: [
{ subject: '实体1', relation: '关系', object: '实体2' }
]
}
});
const result = await tool.execute('call-10', {
action: 'introspect',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.entityCount).toBeGreaterThan(0);
expect(parsed.data.relationCount).toBeGreaterThan(0);
expect(parsed.data.sessionId).toBeDefined();
});
it('should update persona attributes', async () => {
const result = await tool.execute('call-11', {
action: 'persona_update',
params: {
attributes: [
{ attribute: 'name', value: 'AI助手' },
{ attribute: '性格', value: '友善' }
],
mode: 'merge'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('success');
expect(parsed.data.updatedAttributes).toBe(2);
});
it('should clear persona when confirm is true', async () => {
const result = await tool.execute('call-12', {
action: 'persona_clear',
params: {
confirm: true
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('success');
expect(parsed.data.deletedCount).toBeGreaterThanOrEqual(0);
});
it('should cancel when confirm is false', async () => {
const result = await tool.execute('call-13', {
action: 'persona_clear',
params: {
confirm: false
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('cancelled');
expect(parsed.data.deletedCount).toBe(0);
});
it('should create task successfully', async () => {
const result = await tool.execute('call-14', {
action: 'task_create',
params: {
task_id: 'Task_Test123',
description: '测试任务描述'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('success');
expect(parsed.data.taskId).toBe('Task_Test123');
});
it('should create task with info_nodes', async () => {
const result = await tool.execute('call-15', {
action: 'task_create',
params: {
task_id: 'Task_WithInfo',
description: '带信息的任务',
info_nodes: ['信息节点1', '信息节点2']
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.taskId).toBe('Task_WithInfo');
});
it('should set task state', async () => {
await tool.execute('call-16', {
action: 'task_create',
params: {
task_id: 'Task_StateTest',
description: '状态测试任务'
}
});
const result = await tool.execute('call-17', {
action: 'task_set_state',
params: {
task_id: 'Task_StateTest',
state: '已完成'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('success');
expect(parsed.data.newState).toBe('已完成');
});
it('should delete task', async () => {
await tool.execute('call-18', {
action: 'task_create',
params: {
task_id: 'Task_DeleteMe',
description: '待删除任务'
}
});
const result = await tool.execute('call-19', {
action: 'task_delete',
params: {
task_id: 'Task_DeleteMe'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('success');
expect(parsed.data.taskId).toBe('Task_DeleteMe');
});
it('should link info node to task', async () => {
await tool.execute('call-20', {
action: 'task_create',
params: {
task_id: 'Task_Link',
description: '链接测试任务'
}
});
const result = await tool.execute('call-21', {
action: 'task_link_info',
params: {
task_id: 'Task_Link',
info_node: '新的信息节点'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('success');
});
it('should archive old relations', async () => {
const result = await tool.execute('call-22', {
action: 'archive',
params: {
days: 30
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
it('should cleanup in dry_run mode', async () => {
const result = await tool.execute('call-23', {
action: 'cleanup',
params: {
dry_run: true
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
it('should cleanup without dry_run', async () => {
const result = await tool.execute('call-24', {
action: 'cleanup',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
});
it('should return error for unknown action', async () => {
const result = await tool.execute('call-25', {
action: 'invalid_action',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error).toBeDefined();
});
it('should handle errors in execute format', async () => {
const result = await tool.execute('call-26', {
action: '',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
});
it('should handle limiter blocking', async () => {
const newTool = createGraphMemoryTool(TEST_DB_PATH + '.limiter', 'limiter-session');
// Fill up memory_query limit
for (let i = 0; i < 20; i++) {
await newTool.execute(`limiter-call-${i}`, {
action: 'introspect',
params: {}
});
}
// Next recall should be blocked
const result = await newTool.execute('limiter-blocked', {
action: 'recall',
params: { queryIntent: '测试' }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('已达上限');
});
});
describe('ToolLimiter standalone', () => {
it('should limit memory query calls', () => {
const limiter = new ToolLimiter();
for (let i = 0; i < 20; i++) {
const [allowed] = limiter.canCall('recall');
if (allowed) limiter.recordCall('recall');
}
const [allowed] = limiter.canCall('recall');
expect(allowed).toBe(false);
});
it('should limit memory update calls', () => {
const limiter = new ToolLimiter();
for (let i = 0; i < 10; i++) {
const [allowed] = limiter.canCall('commit');
if (allowed) limiter.recordCall('commit');
}
const [allowed] = limiter.canCall('commit');
expect(allowed).toBe(false);
});
it('should get summary', () => {
const limiter = new ToolLimiter();
const summary = limiter.getSummary();
expect(summary).toContain('人设图');
expect(summary).toContain('工作记忆链');
expect(summary).toContain('一般记忆');
});
it('should reset and allow calls again', () => {
const limiter = new ToolLimiter();
for (let i = 0; i < 20; i++) {
const [allowed] = limiter.canCall('recall');
if (allowed) limiter.recordCall('recall');
}
expect(limiter.canCall('recall')[0]).toBe(false);
limiter.reset();
expect(limiter.canCall('recall')[0]).toBe(true);
});
});
describe('Parameter validation', () => {
it('should reject commit without triplets', async () => {
const result = await tool.execute('call-val-1', {
action: 'commit',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('triplets');
});
it('should reject commit with empty triplet fields', async () => {
const result = await tool.execute('call-val-2', {
action: 'commit',
params: {
triplets: [{ subject: '', relation: '是', object: '测试' }]
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('subject');
});
it('should reject persona_update without attributes', async () => {
const result = await tool.execute('call-val-3', {
action: 'persona_update',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('attributes');
});
it('should reject persona_clear without confirm', async () => {
const result = await tool.execute('call-val-4', {
action: 'persona_clear',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('confirm');
});
it('should allow persona_clear with confirm=false and return cancelled', async () => {
const result = await tool.execute('call-val-4b', {
action: 'persona_clear',
params: { confirm: false }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.status).toBe('cancelled');
expect(parsed.data.deletedCount).toBe(0);
});
it('should reject task_create without task_id', async () => {
const result = await tool.execute('call-val-5', {
action: 'task_create',
params: { description: '测试' }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('task_id');
});
it('should reject task_set_state without state', async () => {
const result = await tool.execute('call-val-6', {
action: 'task_set_state',
params: { task_id: 'T1' }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('state');
});
it('should reject recall without queryIntent and seedEntities', async () => {
const result = await tool.execute('call-val-7', {
action: 'recall',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('queryIntent');
});
it('should reject depth out of range', async () => {
const result = await tool.execute('call-val-8', {
action: 'recall',
params: { queryIntent: '测试', depth: 10 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('depth');
});
it('should reject commit with invalid confidence', async () => {
const result = await tool.execute('call-val-9', {
action: 'commit',
params: {
triplets: [{ subject: 'A', relation: '是', object: 'B', confidence: 2 }]
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('置信度');
});
it('should reject purge supersede without newRelation', async () => {
const result = await tool.execute('call-val-10', {
action: 'purge',
params: { mode: 'supersede' }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('newRelation');
});
it('should reject context_rewrite without context', async () => {
const result = await tool.execute('call-val-12', {
action: 'context_rewrite',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('context');
});
it('should reject context_rewrite with invalid maxEntities', async () => {
const result = await tool.execute('call-val-13', {
action: 'context_rewrite',
params: { context: '测试', maxEntities: 200 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('maxEntities');
});
it('should reject working_memory_chain with invalid maxDepth', async () => {
const result = await tool.execute('call-val-14', {
action: 'working_memory_chain',
params: { maxDepth: 10 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('maxDepth');
});
it('should reject missing action', async () => {
const result = await tool.execute('call-val-15', {
action: '',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(false);
expect(parsed.error.message).toContain('action');
});
});
describe('Context rewrite', () => {
it('should compress context and extract entities', async () => {
const longContext = '我们在开发一个项目。这个项目使用 TypeScript。TypeScript 是 JavaScript 的超集。我们在写代码。代码在仓库里。仓库用 Git 管理。Git 是版本控制系统。';
const result = await tool.execute('call-cr-1', {
action: 'context_rewrite',
params: {
context: longContext,
maxEntities: 10
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.extractedEntities).toBeGreaterThan(0);
expect(parsed.data.summary).toBeTruthy();
expect(parsed.data.compressed).toBe(true);
});
it('should use provided summary', async () => {
const result = await tool.execute('call-cr-2', {
action: 'context_rewrite',
params: {
context: '测试文本',
summary: '用户自定义摘要'
}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.summary).toBe('用户自定义摘要');
});
});
describe('Working memory chain', () => {
it('should return working memory chain', async () => {
// 先写入一些数据
await tool.execute('call-wmc-1', {
action: 'commit',
params: {
triplets: [
{ subject: '用户', relation: '喜欢', object: 'Python' },
{ subject: '用户', relation: '正在学习', object: 'TypeScript' }
]
}
});
const result = await tool.execute('call-wmc-2', {
action: 'working_memory_chain',
params: {}
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.chain).toBeDefined();
expect(Array.isArray(parsed.data.chain)).toBe(true);
});
it('should respect maxDepth parameter', async () => {
const result = await tool.execute('call-wmc-3', {
action: 'working_memory_chain',
params: { maxDepth: 2 }
});
const parsed = JSON.parse(result.content[0].text);
expect(parsed.success).toBe(true);
expect(parsed.data.entityCount).toBeDefined();
});
});
});

View File

@ -1,433 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { ToolLimiter } from '../../../../dist/runtime/core/tools/tool_limiter.js';
describe('ToolLimiter', () => {
describe('Default limits', () => {
it('should have correct default limits', () => {
const limiter = new ToolLimiter();
expect(limiter.limits.persona_query_max).toBe(1);
expect(limiter.limits.persona_update_max).toBe(1);
expect(limiter.limits.task_query_max).toBe(4);
expect(limiter.limits.task_update_max).toBe(5);
expect(limiter.limits.memory_query_max).toBe(20);
expect(limiter.limits.memory_update_max).toBe(10);
});
it('should initialize counts to zero', () => {
const limiter = new ToolLimiter();
expect(limiter.counts.persona_query).toBe(0);
expect(limiter.counts.persona_update).toBe(0);
expect(limiter.counts.task_query).toBe(0);
expect(limiter.counts.task_update).toBe(0);
expect(limiter.counts.memory_query).toBe(0);
expect(limiter.counts.memory_update).toBe(0);
});
});
describe('canCall allows within limits', () => {
let limiter: ToolLimiter;
beforeEach(() => {
limiter = new ToolLimiter();
});
it('should allow recall (memory query) initially', () => {
const [allowed, reason] = limiter.canCall('recall');
expect(allowed).toBe(true);
expect(reason).toBe('允许调用');
});
it('should allow introspect (memory query) initially', () => {
const [allowed, reason] = limiter.canCall('introspect');
expect(allowed).toBe(true);
expect(reason).toBe('允许调用');
});
it('should allow commit (memory update) initially', () => {
const [allowed, reason] = limiter.canCall('commit');
expect(allowed).toBe(true);
expect(reason).toBe('允许调用');
});
it('should allow task operations initially', () => {
expect(limiter.canCall('task_create')[0]).toBe(true);
expect(limiter.canCall('task_set_state')[0]).toBe(true);
expect(limiter.canCall('task_delete')[0]).toBe(true);
expect(limiter.canCall('task_link_info')[0]).toBe(true);
});
it('should allow persona operations initially', () => {
expect(limiter.canCall('persona_update')[0]).toBe(true);
expect(limiter.canCall('persona_clear')[0]).toBe(true);
});
it('should allow purge initially', () => {
const [allowed] = limiter.canCall('purge');
expect(allowed).toBe(true);
});
it('should allow archive initially', () => {
const [allowed] = limiter.canCall('archive');
expect(allowed).toBe(true);
});
it('should allow cleanup initially', () => {
const [allowed] = limiter.canCall('cleanup');
expect(allowed).toBe(true);
});
});
describe('canCall blocks when exceeded', () => {
let limiter: ToolLimiter;
beforeEach(() => {
limiter = new ToolLimiter();
});
it('should block recall after 20 calls', () => {
for (let i = 0; i < 20; i++) {
limiter.recordCall('recall');
}
const [allowed, reason] = limiter.canCall('recall');
expect(allowed).toBe(false);
expect(reason).toContain('一般记忆查询次数已达上限');
expect(reason).toContain('20');
});
it('should block commit after 10 calls', () => {
for (let i = 0; i < 10; i++) {
limiter.recordCall('commit');
}
const [allowed, reason] = limiter.canCall('commit');
expect(allowed).toBe(false);
expect(reason).toContain('一般记忆修改次数已达上限');
});
it('should block purge after 10 calls', () => {
for (let i = 0; i < 10; i++) {
limiter.recordCall('purge');
}
const [allowed] = limiter.canCall('purge');
expect(allowed).toBe(false);
});
it('should block task operations after 5 updates', () => {
for (let i = 0; i < 5; i++) {
limiter.recordCall('task_create');
}
const [allowed] = limiter.canCall('task_create');
expect(allowed).toBe(false);
});
it('should block task_set_state after 5 updates', () => {
for (let i = 0; i < 5; i++) {
limiter.recordCall('task_set_state');
}
const [allowed] = limiter.canCall('task_set_state');
expect(allowed).toBe(false);
});
it('should block persona_update after 1 call', () => {
limiter.recordCall('persona_update');
const [allowed] = limiter.canCall('persona_update');
expect(allowed).toBe(false);
});
it('should block persona_clear after 1 call', () => {
limiter.recordCall('persona_clear');
const [allowed] = limiter.canCall('persona_clear');
expect(allowed).toBe(false);
});
it('should block unknown actions as memory update', () => {
for (let i = 0; i < 10; i++) {
limiter.recordCall('unknown_action');
}
const [allowed] = limiter.canCall('unknown_action');
expect(allowed).toBe(false);
});
});
describe('recordCall increments counters', () => {
let limiter: ToolLimiter;
beforeEach(() => {
limiter = new ToolLimiter();
});
it('should increment memory_query for recall', () => {
limiter.recordCall('recall');
expect(limiter.counts.memory_query).toBe(1);
limiter.recordCall('recall');
expect(limiter.counts.memory_query).toBe(2);
});
it('should increment memory_query for introspect', () => {
limiter.recordCall('introspect');
expect(limiter.counts.memory_query).toBe(1);
});
it('should increment memory_update for commit', () => {
limiter.recordCall('commit');
expect(limiter.counts.memory_update).toBe(1);
});
it('should increment memory_update for purge', () => {
limiter.recordCall('purge');
expect(limiter.counts.memory_update).toBe(1);
});
it('should increment memory_update for archive', () => {
limiter.recordCall('archive');
expect(limiter.counts.memory_update).toBe(1);
});
it('should increment memory_update for cleanup', () => {
limiter.recordCall('cleanup');
expect(limiter.counts.memory_update).toBe(1);
});
it('should increment task_update for task operations', () => {
limiter.recordCall('task_create');
limiter.recordCall('task_set_state');
limiter.recordCall('task_delete');
limiter.recordCall('task_link_info');
expect(limiter.counts.task_update).toBe(4);
});
it('should increment persona_update for persona operations', () => {
limiter.recordCall('persona_update');
limiter.recordCall('persona_clear');
expect(limiter.counts.persona_update).toBe(2);
});
it('should handle unknown actions as memory update', () => {
limiter.recordCall('some_unknown_action');
expect(limiter.counts.memory_update).toBe(1);
});
});
describe('reset clears counters', () => {
it('should reset all counts to zero', () => {
const limiter = new ToolLimiter();
limiter.recordCall('recall');
limiter.recordCall('recall');
limiter.recordCall('commit');
limiter.recordCall('task_create');
limiter.recordCall('persona_update');
limiter.reset();
expect(limiter.counts.persona_query).toBe(0);
expect(limiter.counts.persona_update).toBe(0);
expect(limiter.counts.task_query).toBe(0);
expect(limiter.counts.task_update).toBe(0);
expect(limiter.counts.memory_query).toBe(0);
expect(limiter.counts.memory_update).toBe(0);
});
it('should allow calls after reset', () => {
const limiter = new ToolLimiter();
for (let i = 0; i < 20; i++) {
limiter.recordCall('recall');
}
limiter.reset();
const [allowed] = limiter.canCall('recall');
expect(allowed).toBe(true);
});
});
describe('getSummary returns formatted string', () => {
it('should return summary with correct format', () => {
const limiter = new ToolLimiter();
const summary = limiter.getSummary();
expect(summary).toContain('人设图');
expect(summary).toContain('工作记忆链');
expect(summary).toContain('一般记忆');
expect(summary).toContain('查询');
expect(summary).toContain('修改');
expect(summary).toContain('/');
});
it('should show updated counts in summary', () => {
const limiter = new ToolLimiter();
limiter.recordCall('recall');
limiter.recordCall('recall');
limiter.recordCall('commit');
const summary = limiter.getSummary();
expect(summary).toContain('2/20');
expect(summary).toContain('1/10');
});
});
describe('classifyTool for all action types', () => {
let limiter: ToolLimiter;
beforeEach(() => {
limiter = new ToolLimiter();
});
it('should classify recall as memory query', () => {
for (let i = 0; i < 20; i++) limiter.recordCall('recall');
const [, reason] = limiter.canCall('recall');
expect(reason).toContain('一般记忆查询');
});
it('should classify introspect as memory query', () => {
for (let i = 0; i < 20; i++) limiter.recordCall('introspect');
const [, reason] = limiter.canCall('introspect');
expect(reason).toContain('一般记忆查询');
});
it('should classify commit as memory update', () => {
for (let i = 0; i < 10; i++) limiter.recordCall('commit');
const [, reason] = limiter.canCall('commit');
expect(reason).toContain('一般记忆修改');
});
it('should classify purge as memory update', () => {
for (let i = 0; i < 10; i++) limiter.recordCall('purge');
const [, reason] = limiter.canCall('purge');
expect(reason).toContain('一般记忆修改');
});
it('should classify archive as memory update', () => {
for (let i = 0; i < 10; i++) limiter.recordCall('archive');
const [, reason] = limiter.canCall('archive');
expect(reason).toContain('一般记忆修改');
});
it('should classify cleanup as memory update', () => {
for (let i = 0; i < 10; i++) limiter.recordCall('cleanup');
const [, reason] = limiter.canCall('cleanup');
expect(reason).toContain('一般记忆修改');
});
it('should classify task_create as task update', () => {
for (let i = 0; i < 5; i++) limiter.recordCall('task_create');
const [, reason] = limiter.canCall('task_create');
expect(reason).toContain('工作记忆链修改');
});
it('should classify task_set_state as task update', () => {
for (let i = 0; i < 5; i++) limiter.recordCall('task_set_state');
const [, reason] = limiter.canCall('task_set_state');
expect(reason).toContain('工作记忆链修改');
});
it('should classify task_delete as task update', () => {
for (let i = 0; i < 5; i++) limiter.recordCall('task_delete');
const [, reason] = limiter.canCall('task_delete');
expect(reason).toContain('工作记忆链修改');
});
it('should classify task_link_info as task update', () => {
for (let i = 0; i < 5; i++) limiter.recordCall('task_link_info');
const [, reason] = limiter.canCall('task_link_info');
expect(reason).toContain('工作记忆链修改');
});
it('should classify persona_update as persona update', () => {
limiter.recordCall('persona_update');
const [, reason] = limiter.canCall('persona_update');
expect(reason).toContain('人设图修改');
});
it('should classify persona_clear as persona update', () => {
limiter.recordCall('persona_clear');
const [, reason] = limiter.canCall('persona_clear');
expect(reason).toContain('人设图修改');
});
it('should classify unknown action as memory update', () => {
for (let i = 0; i < 10; i++) limiter.recordCall('totally_unknown');
const [, reason] = limiter.canCall('totally_unknown');
expect(reason).toContain('一般记忆修改');
});
});
describe('Custom limits override', () => {
it('should accept custom limits', () => {
const limiter = new ToolLimiter({
memory_query_max: 100,
memory_update_max: 50
});
expect(limiter.limits.memory_query_max).toBe(100);
expect(limiter.limits.memory_update_max).toBe(50);
expect(limiter.limits.task_query_max).toBe(4);
});
it('should merge custom limits with defaults', () => {
const limiter = new ToolLimiter({
persona_update_max: 5
});
expect(limiter.limits.persona_update_max).toBe(5);
expect(limiter.limits.persona_query_max).toBe(1);
});
it('should allow unlimited calls with high custom limits', () => {
const limiter = new ToolLimiter({
memory_query_max: 10000,
memory_update_max: 10000
});
for (let i = 0; i < 100; i++) {
limiter.recordCall('recall');
limiter.recordCall('commit');
}
expect(limiter.canCall('recall')[0]).toBe(true);
expect(limiter.canCall('commit')[0]).toBe(true);
});
it('should allow zero limits to block immediately', () => {
const limiter = new ToolLimiter({
task_update_max: 0
});
const [allowed] = limiter.canCall('task_create');
expect(allowed).toBe(false);
});
it('should preserve custom limits after reset', () => {
const limiter = new ToolLimiter({
memory_query_max: 500
});
for (let i = 0; i < 100; i++) {
limiter.recordCall('recall');
}
limiter.reset();
expect(limiter.limits.memory_query_max).toBe(500);
expect(limiter.counts.memory_query).toBe(0);
const [allowed] = limiter.canCall('recall');
expect(allowed).toBe(true);
});
});
});

View File

@ -1,188 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import Database from 'better-sqlite3';
vi.mock('@xenova/transformers', () => ({
pipeline: vi.fn().mockImplementation(() => {
const mockModel = (text: string, opts: any) => {
const arr = new Float32Array(384);
const seed = text.split('').reduce((a, c) => a + c.charCodeAt(0), 0);
for (let i = 0; i < 384; i++) {
arr[i] = Math.sin((seed + i) * 0.1);
}
return { data: arr };
};
mockModel.to = function() {
return this;
};
return mockModel;
}),
}));
import { SemanticSearchEngine, cosineSimilarity } from '/home/program/TrulyMEM-TrueHumanMEM/ts/dist/runtime/core/graph_memory/semantic_search.js';
const TEST_DB_PATH = '/tmp/test_semantic_search.db';
describe('SemanticSearchEngine', () => {
let db: Database.Database;
let engine: SemanticSearchEngine;
beforeEach(async () => {
try {
const fs = await import('fs');
if (fs.existsSync(TEST_DB_PATH)) {
fs.unlinkSync(TEST_DB_PATH);
}
if (fs.existsSync(`${TEST_DB_PATH}-wal`)) {
fs.unlinkSync(`${TEST_DB_PATH}-wal`);
}
if (fs.existsSync(`${TEST_DB_PATH}-shm`)) {
fs.unlinkSync(`${TEST_DB_PATH}-shm`);
}
} catch {}
db = new Database(TEST_DB_PATH);
engine = new SemanticSearchEngine(db);
});
afterEach(() => {
if (db) {
db.close();
}
});
describe('initialization', () => {
it('creates embeddings table on initialization', () => {
const tableInfo = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
expect(tableInfo).toBeDefined();
});
it('creates index on source column', () => {
const indexInfo = db.prepare("SELECT name FROM sqlite_master WHERE type='index' AND name='idx_embeddings_source'").get();
expect(indexInfo).toBeDefined();
});
});
describe('generateEmbedding', () => {
it('generates 384 dimensional embedding', async () => {
const embedding = await engine.generateEmbedding('hello world');
expect(embedding.length).toBe(384);
});
it('generates consistent embeddings for same text', async () => {
const emb1 = await engine.generateEmbedding('test');
const emb2 = await engine.generateEmbedding('test');
expect(emb1.length).toBe(emb2.length);
const similarity = cosineSimilarity(emb1, emb2);
expect(similarity).toBeCloseTo(1, 3);
});
it('generates different embeddings for different text', async () => {
const emb1 = await engine.generateEmbedding('hello');
const emb2 = await engine.generateEmbedding('world');
const similarity = cosineSimilarity(emb1, emb2);
expect(similarity).not.toBe(1);
});
});
describe('storeEmbedding', () => {
it('stores embedding to SQLite', async () => {
const embedding = await engine.generateEmbedding('test');
engine.storeEmbedding('test-id', 'test text', embedding, 'test-source', 1);
const row = db.prepare('SELECT * FROM embeddings WHERE id = ?').get('test-id') as any;
expect(row).toBeDefined();
expect(row.text).toBe('test text');
expect(row.source).toBe('test-source');
expect(row.source_line).toBe(1);
});
it('replaces existing embedding with same id', async () => {
const emb1 = await engine.generateEmbedding('text1');
engine.storeEmbedding('dup-id', 'text 1', emb1);
const emb2 = await engine.generateEmbedding('text2');
engine.storeEmbedding('dup-id', 'text 2', emb2);
const row = db.prepare('SELECT text FROM embeddings WHERE id = ?').get('dup-id') as any;
expect(row.text).toBe('text 2');
});
});
describe('searchSimilar', () => {
beforeEach(async () => {
const emb1 = await engine.generateEmbedding('machine learning');
const emb2 = await engine.generateEmbedding('deep learning');
const emb3 = await engine.generateEmbedding('hello world');
engine.storeEmbedding('item-1', 'machine learning', emb1, 'source1', 1);
engine.storeEmbedding('item-2', 'deep learning neural network', emb2, 'source2', 2);
engine.storeEmbedding('item-3', 'hello world', emb3, 'source3', 3);
});
it('finds similar embeddings using cosine similarity', async () => {
const query = await engine.generateEmbedding('neural networks');
const results = engine.searchSimilar(query, 10);
expect(results.length).toBeGreaterThan(0);
expect(results[0].id).toBeDefined();
expect(typeof results[0].similarity).toBe('number');
});
it('returns results sorted by similarity descending', async () => {
const query = await engine.generateEmbedding('training');
const results = engine.searchSimilar(query, 10);
for (let i = 1; i < results.length; i++) {
expect(results[i - 1].similarity).toBeGreaterThanOrEqual(results[i].similarity);
}
});
it('respects limit parameter', async () => {
const query = await engine.generateEmbedding('test query');
const results = engine.searchSimilar(query, 2);
expect(results.length).toBeLessThanOrEqual(2);
});
it('includes source and sourceLine in results', async () => {
const query = await engine.generateEmbedding('test');
const results = engine.searchSimilar(query, 1);
expect(results[0].source).toBeDefined();
});
});
describe('cosineSimilarity', () => {
it('returns 1 for identical vectors', () => {
const a = new Float32Array([1, 0, 0]);
const b = new Float32Array([1, 0, 0]);
expect(cosineSimilarity(a, b)).toBeCloseTo(1);
});
it('returns -1 for opposite vectors', () => {
const a = new Float32Array([1, 0, 0]);
const b = new Float32Array([-1, 0, 0]);
expect(cosineSimilarity(a, b)).toBeCloseTo(-1);
});
it('returns 0 for orthogonal vectors', () => {
const a = new Float32Array([1, 0, 0]);
const b = new Float32Array([0, 1, 0]);
expect(cosineSimilarity(a, b)).toBeCloseTo(0);
});
it('returns value between -1 and 1 for random vectors', () => {
const a = new Float32Array([0.5, 0.3, 0.7]);
const b = new Float32Array([0.2, 0.8, 0.1]);
const similarity = cosineSimilarity(a, b);
expect(similarity).toBeGreaterThanOrEqual(-1);
expect(similarity).toBeLessThanOrEqual(1);
});
it('handles 384 dimensional vectors', () => {
const a = new Float32Array(384).fill(0.1);
const b = new Float32Array(384).fill(0.1);
const similarity = cosineSimilarity(a, b);
expect(similarity).toBeCloseTo(1);
});
});
});

View File

@ -12,12 +12,12 @@
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"exactOptionalPropertyTypes": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"exactOptionalPropertyTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"resolveJsonModule": true
"typeRoots": ["./src/types", "./node_modules/@types"]
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]