Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,43 @@ docker exec -it hermes-memory hermes

---

### 3. Existing Hermes Agent

If Hermes is already installed on the host, install the memory provider into that existing agent instead of using the Docker image. The installer expects Hermes under `~/.hermes/hermes-agent`; when running as root for another account, pass `INSTALL_AS_USER=<user>`.

```bash
# Download the npm package globally so the install script is available.
npm install -g @tencentdb-agent-memory/memory-tencentdb

# Link memory_tencentdb into the existing Hermes plugin directory.
INSTALL_SCRIPT="$(npm root -g)/@tencentdb-agent-memory/memory-tencentdb/scripts/install_hermes_memory_tencentdb.sh"
bash "$INSTALL_SCRIPT"

# Configure the Gateway used by the Hermes provider.
CTL="$HOME/.memory-tencentdb/tdai-memory-openclaw-plugin/scripts/memory-tencentdb-ctl.sh"
bash "$CTL" --hermes config llm \
--api-key "sk-..." \
--base-url "https://api.openai.com/v1" \
--model "gpt-4o"

# Optional: enable remote embedding. ZeroEntropy uses its native embed API.
bash "$CTL" --hermes config embedding \
--provider zeroentropy \
--api-key "$ZEROENTROPY_API_KEY" \
--base-url "https://api.zeroentropy.dev" \
--model "zembed-1" \
--dimensions 2560 \
--restart

# Enable the Hermes memory provider and verify the Gateway.
bash "$CTL" --hermes enable-hermes-memory
curl http://127.0.0.1:8420/health
```

For OpenAI-compatible embedding providers, use the same `config embedding` command with `--provider openai` or your provider name, an OpenAI-compatible `--base-url`, model, and dimensions.

---


## 🔧 Configurable Parameters

Expand Down Expand Up @@ -288,9 +325,9 @@ docker exec -it hermes-memory hermes
<details>
<summary><b>🔴 Level 3 · Full parameter reference</b> (ops / custom models / remote embedding)</summary>

For all fields, types, and constraints see [`openclaw.plugin.json`](./openclaw.plugin.json)
For all fields, types, and constraints see [`openclaw.plugin.json`](./openclaw.plugin.json).

- `embedding.*` — remote embedding service (OpenAI-compatible API)
- `embedding.*` — remote embedding service (OpenAI-compatible API or ZeroEntropy native API)
- `llm.*` — standalone LLM mode (bypass OpenClaw's built-in model and run L1/L2/L3 with a designated API)
- `offload.backendUrl / backendApiKey` — offload the L1/L1.5/L2/L4 flow to a backend service
- `report.*` — metrics reporting
Expand Down
39 changes: 38 additions & 1 deletion README_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,43 @@ docker exec -it hermes-memory hermes
> 镜像内置了腾讯云 DeepSeek-V3.2 的默认值,如果你使用该模型,`MODEL_BASE_URL`/`MODEL_NAME`/`MODEL_PROVIDER` 可以省略,只传 `MODEL_API_KEY` 即可。


---

### 3. 接入已有 Hermes Agent

如果机器上已经安装了 Hermes,不需要改用 Docker 镜像,可以把 `memory_tencentdb` provider 安装到现有 Hermes。安装脚本默认查找 `~/.hermes/hermes-agent`;如果用 root 给其他用户安装,请传 `INSTALL_AS_USER=<user>`。

```bash
# 先全局下载 npm 包,拿到安装脚本。
npm install -g @tencentdb-agent-memory/memory-tencentdb

# 将 memory_tencentdb 链接到现有 Hermes 插件目录。
INSTALL_SCRIPT="$(npm root -g)/@tencentdb-agent-memory/memory-tencentdb/scripts/install_hermes_memory_tencentdb.sh"
bash "$INSTALL_SCRIPT"

# 配置 Hermes provider 背后的 Gateway。
CTL="$HOME/.memory-tencentdb/tdai-memory-openclaw-plugin/scripts/memory-tencentdb-ctl.sh"
bash "$CTL" --hermes config llm \
--api-key "sk-..." \
--base-url "https://api.openai.com/v1" \
--model "gpt-4o"

# 可选:开启远程 embedding。ZeroEntropy 会走原生 embed API。
bash "$CTL" --hermes config embedding \
--provider zeroentropy \
--api-key "$ZEROENTROPY_API_KEY" \
--base-url "https://api.zeroentropy.dev" \
--model "zembed-1" \
--dimensions 2560 \
--restart

# 启用 Hermes memory provider,并验证 Gateway。
bash "$CTL" --hermes enable-hermes-memory
curl http://127.0.0.1:8420/health
```

如果使用 OpenAI 兼容的 embedding provider,同样执行 `config embedding`,把 `--provider` 换成 `openai` 或对应 provider 名称,并填写兼容 OpenAI 的 `--base-url`、模型名和维度。

---

## 🔧 可调参数
Expand Down Expand Up @@ -293,7 +330,7 @@ docker exec -it hermes-memory hermes

完整字段、类型、约束见 [`openclaw.plugin.json`](./openclaw.plugin.json) 。

- `embedding.*` — 远程 embedding 服务(OpenAI 兼容 API)
- `embedding.*` — 远程 embedding 服务(OpenAI 兼容 API 或 ZeroEntropy 原生 API
- `llm.*` — 独立 LLM 模式(绕过 OpenClaw 内置模型,用指定 API 跑 L1/L2/L3)
- `offload.backendUrl / backendApiKey` — 将 L1/L1.5/L2/L4 offload 流程卸载到后端服务
- `report.*` — 指标上报
Expand Down
2 changes: 2 additions & 0 deletions bin/memory-tdai.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import "../dist/cli.mjs";
11 changes: 8 additions & 3 deletions openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"properties": {
"everyNConversations": { "type": "number", "default": 5, "description": "每 N 轮对话触发 L1 批处理" },
"enableWarmup": { "type": "boolean", "default": true, "description": "Warm-up 模式:新 session 从 1 轮触发开始,每次 L1 后翻倍(1→2→4→...→everyN),加速早期记忆提取" },
"enableL2": { "type": "boolean", "default": true, "description": "是否启用 L2 场景归纳阶段。关闭后只保留 L0/L1 记忆,不再触发场景抽取。" },
"enableL3": { "type": "boolean", "default": true, "description": "是否启用 L3 用户画像生成阶段。关闭后 L2 完成也不会触发画像生成。" },
"l1IdleTimeoutSeconds": { "type": "number", "default": 600, "description": "L1 空闲超时(秒):用户停止对话后多久触发 L1 批处理" },
"l2DelayAfterL1Seconds": { "type": "number", "default": 10, "description": "L1 完成后延迟多久触发 L2(秒)" },
"l2MinIntervalSeconds": { "type": "number", "default": 900, "description": "同一 session 两次 L2 抽取的最小间隔(秒)" },
Expand All @@ -79,7 +81,7 @@
"description": "向量搜索 (Embedding) 配置",
"properties": {
"enabled": { "type": "boolean", "default": true, "description": "是否启用向量搜索(若 provider=none,则实际会被禁用)" },
"provider": { "type": "string", "default": "none", "description": "Embedding 服务提供者:填写兼容 OpenAI API 的远端服务名称(如 openai、deepseek 等);不填或填 none 则禁用向量搜索" },
"provider": { "type": "string", "default": "none", "description": "Embedding 服务提供者:填写远端服务名称(如 openai、deepseek、zeroentropy、qclaw 等);zeroentropy 使用原生 /v1/models/embed 接口,其余默认按 OpenAI 兼容接口调用;不填或填 none 则禁用向量搜索" },
"proxyUrl": { "type": "string", "description": "本地代理地址(仅 provider=qclaw 时必填)。配置后 embedding 请求将通过该代理转发,原始 baseUrl 作为 Remote-URL 头传递" },
"baseUrl": { "type": "string", "description": "API Base URL(必填):填写对应 provider 的 API 地址" },
"apiKey": { "type": "string", "description": "API Key(必填)" },
Expand Down Expand Up @@ -131,7 +133,8 @@
"apiKey": { "type": "string", "description": "API Key" },
"model": { "type": "string", "default": "gpt-4o", "description": "模型名称(如 gpt-4o, deepseek-v3, claude-sonnet-4-6)" },
"maxTokens": { "type": "number", "default": 4096, "description": "最大输出 token 数" },
"timeoutMs": { "type": "number", "default": 120000, "description": "请求超时(毫秒)" }
"timeoutMs": { "type": "number", "default": 120000, "description": "请求超时(毫秒)" },
"providerOptions": { "type": "object", "additionalProperties": true, "description": "透传给 AI SDK providerOptions 的供应商专属参数,例如关闭 thinking 或设置 extraBody。" }
}
},
"offload": {
Expand All @@ -152,7 +155,9 @@
"mmdMaxTokenRatio": { "type": "number", "default": 0.2, "description": "MMD 注入 token 预算比例" },
"backendUrl": { "type": "string", "description": "后端服务 URL(如 https://offload-api.example.com),配置后 L1/L1.5/L2/L4 走后端" },
"backendApiKey": { "type": "string", "description": "后端 API 认证 token" },
"backendTimeoutMs": { "type": "number", "default": 10000, "description": "后端调用超时(毫秒)" }
"backendTimeoutMs": { "type": "number", "default": 10000, "description": "后端调用超时(毫秒)" },
"allowInsecureTls": { "type": "boolean", "default": false, "description": "是否显式允许 HTTPS 后端跳过证书校验。默认 false;仅本地调试或内网自签证书临时使用。" },
"backendCaPemPath": { "type": "string", "description": "HTTPS 后端自定义 CA 证书 PEM 路径。优先用于自签/企业 CA,避免关闭证书校验。" }
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"main": "./dist/index.mjs",
"bin": {
"memory-tdai": "./bin/memory-tdai.mjs",
"migrate-sqlite-to-tcvdb": "./bin/migrate-sqlite-to-tcvdb.mjs",
"export-tencent-vdb": "./bin/export-tencent-vdb.mjs",
"read-local-memory": "./bin/read-local-memory.mjs"
Expand Down Expand Up @@ -34,14 +35,12 @@
"files": [
"dist/",
"bin/",
"index.ts",
"scripts/migrate-sqlite-to-tcvdb/dist/",
"scripts/export-tencent-vdb/dist/",
"scripts/read-local-memory/dist/",
"scripts/memory-tencentdb-ctl.sh",
"scripts/install_hermes_memory_tencentdb.sh",
"scripts/README.memory-tencentdb-ctl.md",
"src/",
"scripts/openclaw-after-tool-call-messages.patch.sh",
"scripts/setup-offload.sh",
"hermes-plugin/",
Expand Down Expand Up @@ -77,6 +76,7 @@
"@node-rs/jieba": "^2.0.1",
"@tencentdb-agent-memory/tcvdb-text": "^0.1.1",
"ai": "^6.0.164",
"commander": "^14.0.3",
"js-tiktoken": "^1.0.18",
"json5": "^2.2.3",
"sqlite-vec": "0.1.7-alpha.2",
Expand All @@ -102,7 +102,7 @@
},
"openclaw": {
"extensions": [
"./index.ts"
"./dist/index.mjs"
],
"compat": {
"pluginApi": ">=2026.3.13",
Expand Down
10 changes: 10 additions & 0 deletions scripts/README.memory-tencentdb-ctl.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,21 @@ memory-tencentdb-ctl config embedding \
--dimensions 1536 \
--restart

# ZeroEntropy 使用原生 /v1/models/embed 接口,base-url 不需要带 /v1
memory-tencentdb-ctl config embedding \
--provider zeroentropy \
--api-key "$ZEROENTROPY_API_KEY" \
--base-url "https://api.zeroentropy.dev" \
--model "zembed-1" \
--dimensions 2560 \
--restart

# 关闭 embedding(退化为 BM25/关键词召回)
memory-tencentdb-ctl config embedding --provider none --restart
```

- JSON 写入点:`$.memory.embedding.{provider, baseUrl, apiKey, model, dimensions, enabled, proxyUrl?}`。
- `zeroentropy` provider 会调用 `POST <baseUrl>/v1/models/embed` 并解析 `results[].embedding`。
- `qclaw` provider 额外要求 `--proxy-url`。
- 校验规则与 `src/config.ts` 的 `parseConfig()` 对齐:`dimensions` 为正整数,非 `none` 必须带 `apiKey/baseUrl/model/dimensions`;缺项直接报错不写半残 JSON。

Expand Down
2 changes: 1 addition & 1 deletion scripts/memory-tencentdb-ctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,7 @@ cmd_config_embedding() {
*) die "config embedding: 未知参数 $1" 1 ;;
esac
done
[[ -n "$provider" ]] || die "--provider 必填(none/openai/deepseek/qclaw/...)"
[[ -n "$provider" ]] || die "--provider 必填(none/openai/deepseek/zeroentropy/qclaw/...)"

# provider=none 只写 provider,其余置空,相当于关闭向量检索
if [[ "$provider" == "none" ]]; then
Expand Down
17 changes: 16 additions & 1 deletion scripts/migrate-sqlite-to-tcvdb/sqlite-to-tcvdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,14 +297,16 @@ function compactTimestamps(row: L1RecordRow): string[] {
function mapL1RowToMemoryRecord(row: L1RecordRow): MemoryRecord {
const timestamps = compactTimestamps(row);
const fallbackIso = row.updated_time || row.created_time || row.timestamp_end || row.timestamp_str || new Date(0).toISOString();
const metadata = safeParseMetadata(row.metadata_json);
return {
id: row.record_id,
content: row.content,
type: row.type as MemoryRecord["type"],
scope: normalizeScope(metadata.scope, row.content, row.type as MemoryRecord["type"]),
priority: row.priority,
scene_name: row.scene_name,
source_message_ids: [],
metadata: safeParseMetadata(row.metadata_json),
metadata,
timestamps,
createdAt: row.created_time || fallbackIso,
updatedAt: row.updated_time || row.created_time || fallbackIso,
Expand All @@ -313,6 +315,19 @@ function mapL1RowToMemoryRecord(row: L1RecordRow): MemoryRecord {
};
}

function normalizeScope(raw: unknown, content: string, type: MemoryRecord["type"]): MemoryRecord["scope"] {
if (raw === "global" || raw === "project" || raw === "session") {
return raw;
}
if (/(这个项目|本项目|当前项目|当前仓库|这个仓库|工作区|PR|issue|腾讯这个项目)/i.test(content)) {
return "project";
}
if (/(这次|本次|当前任务|本轮|临时|今天刚提|刚刚)/i.test(content)) {
return "session";
}
return type === "episodic" ? "session" : "global";
}

function mapL0RowToRecord(row: L0RecordRow): L0Record {
return {
id: row.record_id,
Expand Down
72 changes: 72 additions & 0 deletions src/adapters/standalone/llm-runner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

const generateTextMock = vi.fn();

vi.mock("ai", async (importOriginal) => {
const actual = await importOriginal<typeof import("ai")>();
return {
...actual,
generateText: generateTextMock,
};
});

vi.mock("@ai-sdk/openai", () => ({
createOpenAI: () => ({
chat: (model: string) => ({ model }),
}),
}));

describe("StandaloneLLMRunner", () => {
beforeEach(() => {
generateTextMock.mockReset();
generateTextMock.mockResolvedValue({ text: "[]", steps: [] });
});

it("does not expose tools when enableTools is false", async () => {
const { StandaloneLLMRunner } = await import("./llm-runner.js");
const runner = new StandaloneLLMRunner({
config: {
baseUrl: "https://api.example.test/v1",
apiKey: "test-key",
model: "deepseek-ai/DeepSeek-V4-Flash",
},
enableTools: false,
});

await runner.run({ prompt: "return JSON", taskId: "l1" });

expect(generateTextMock).toHaveBeenCalledOnce();
expect(generateTextMock.mock.calls[0]![0]).not.toHaveProperty("tools");
});

it("passes configured provider options to disable provider thinking modes", async () => {
const { StandaloneLLMRunner } = await import("./llm-runner.js");
const runner = new StandaloneLLMRunner({
config: {
baseUrl: "https://api.example.test/v1",
apiKey: "test-key",
model: "deepseek-ai/DeepSeek-V4-Flash",
providerOptions: {
openai: {
extraBody: {
enable_thinking: false,
},
},
},
},
enableTools: false,
});

await runner.run({ prompt: "return JSON", taskId: "l1" });

expect(generateTextMock.mock.calls[0]![0]).toMatchObject({
providerOptions: {
openai: {
extraBody: {
enable_thinking: false,
},
},
},
});
});
});
30 changes: 12 additions & 18 deletions src/adapters/standalone/llm-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export interface StandaloneLLMConfig {
maxTokens?: number;
/** Request timeout in milliseconds (default: 120_000). */
timeoutMs?: number;
/** Provider-specific options passed through to the AI SDK. */
providerOptions?: Record<string, unknown>;
}

// ============================
Expand Down Expand Up @@ -149,12 +151,6 @@ function createSandboxedTools(workspaceDir: string, logger?: Logger) {
};
}

/** Read-only tool subset — used when enableTools=false to avoid empty tools rejection. */
function createReadOnlyTools(workspaceDir: string, logger?: Logger) {
const all = createSandboxedTools(workspaceDir, logger);
return { read_file: all.read_file };
}

// ============================
// StandaloneLLMRunner
// ============================
Expand Down Expand Up @@ -188,30 +184,28 @@ export class StandaloneLLMRunner implements LLMRunner {
`tools=${this.enableTools}, timeout=${timeoutMs}ms`,
);

// Create OpenAI-compatible provider via AI SDK
// Use "compatible" mode to call /chat/completions (not Responses API),
// which works with all OpenAI-compatible backends (DeepSeek, Qwen, etc.)
// Create OpenAI-compatible provider via AI SDK.
const provider = createOpenAI({
baseURL: this.config.baseUrl,
apiKey: this.config.apiKey,
compatibility: "compatible",
});

// Select tools based on mode
const tools = this.enableTools
? createSandboxedTools(workspaceDir, this.logger)
: createReadOnlyTools(workspaceDir, this.logger);

try {
const result = await generateText({
const request: Parameters<typeof generateText>[0] = {
model: provider.chat(this.model),
system: params.systemPrompt,
prompt: params.prompt,
tools,
stopWhen: stepCountIs(this.enableTools ? MAX_TOOL_ITERATIONS : 1),
maxOutputTokens: maxTokens,
abortSignal: AbortSignal.timeout(timeoutMs),
});
providerOptions: (params.providerOptions ?? this.config.providerOptions) as any,
};

if (this.enableTools) {
request.tools = createSandboxedTools(workspaceDir, this.logger);
}

const result = await generateText(request);

const text = result.text.trim();
const totalMs = Date.now() - runStartMs;
Expand Down
Loading
Loading